summaryrefslogtreecommitdiff
path: root/spec/support/shared_examples
diff options
context:
space:
mode:
Diffstat (limited to 'spec/support/shared_examples')
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/controllers/concerns/integrations_actions_shared_examples.rb59
-rw-r--r--spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/controllers/issuable_anonymous_search_disabled_examples.rb55
-rw-r--r--spec/support/shared_examples/features/atom/issuable_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/features/content_editor_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/features/deploy_token_shared_examples.rb23
-rw-r--r--spec/support/shared_examples/features/discussion_comments_shared_example.rb6
-rw-r--r--spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/manage_applications_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/rss_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb124
-rw-r--r--spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb52
-rw-r--r--spec/support/shared_examples/mailers/notify_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/concerns/featurable_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/models/concerns/sanitizable_shared_examples.rb41
-rw-r--r--spec/support/shared_examples/models/member_shared_examples.rb56
-rw-r--r--spec/support/shared_examples/models/mentionable_shared_examples.rb36
-rw-r--r--spec/support/shared_examples/namespaces/traversal_scope_examples.rb125
-rw-r--r--spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/requests/api/packages_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/requests/rack_attack_shared_examples.rb202
-rw-r--r--spec/support/shared_examples/services/dependency_proxy_ttl_policies_shared_examples.rb34
-rw-r--r--spec/support/shared_examples/services/incident_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/users/dismiss_user_callout_service_shared_examples.rb37
-rw-r--r--spec/support/shared_examples/work_item_base_types_importer.rb10
30 files changed, 944 insertions, 84 deletions
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 cadc753513d..1e303197990 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
@@ -3,14 +3,10 @@
RSpec.shared_examples 'multiple issue boards' do
context 'authorized user' do
before do
- stub_feature_flags(board_new_list: false)
-
parent.add_maintainer(user)
login_as(user)
- stub_feature_flags(board_new_list: false)
-
visit boards_path
wait_for_requests
end
@@ -79,13 +75,13 @@ RSpec.shared_examples 'multiple issue boards' do
expect(page).to have_content(board2.name)
end
- click_button 'Add list'
+ click_button 'Create list'
- wait_for_requests
+ click_button 'Select a label'
- page.within '.dropdown-menu-issues-board-new' do
- click_link planning.title
- end
+ page.choose(planning.title)
+
+ click_button 'Add to board'
wait_for_requests
diff --git a/spec/support/shared_examples/controllers/concerns/integrations_actions_shared_examples.rb b/spec/support/shared_examples/controllers/concerns/integrations_actions_shared_examples.rb
new file mode 100644
index 00000000000..748a3acf17b
--- /dev/null
+++ b/spec/support/shared_examples/controllers/concerns/integrations_actions_shared_examples.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples IntegrationsActions do
+ let(:integration) do
+ create(:datadog_integration,
+ integration_attributes.merge(
+ api_url: 'http://example.com',
+ api_key: 'secret'
+ )
+ )
+ end
+
+ describe 'GET #edit' do
+ before do
+ get :edit, params: routing_params
+ end
+
+ it 'assigns the integration' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:integration)).to eq(integration)
+ end
+ end
+
+ describe 'PUT #update' do
+ let(:params) do
+ {
+ datadog_env: 'env',
+ datadog_service: 'service'
+ }
+ end
+
+ before do
+ put :update, params: routing_params.merge(integration: params)
+ end
+
+ it 'updates the integration with the provided params and redirects to the form' do
+ expect(response).to redirect_to(routing_params.merge(action: :edit))
+ expect(integration.reload).to have_attributes(params)
+ end
+
+ context 'when sending a password field' do
+ let(:params) { super().merge(api_key: 'new') }
+
+ it 'updates the integration with the password and other params' do
+ expect(response).to be_redirect
+ expect(integration.reload).to have_attributes(params)
+ end
+ end
+
+ context 'when sending a blank password field' do
+ let(:params) { super().merge(api_key: '') }
+
+ it 'ignores the password field and saves the other params' do
+ expect(response).to be_redirect
+ expect(integration.reload).to have_attributes(params.merge(api_key: 'secret'))
+ 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 a9c6da7bc2b..0ffa32dec9e 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
@@ -82,16 +82,6 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do
expect(json_response.dig("provider_repos", 1, "id")).to eq(org_repo.id)
end
- it "does not show already added project" do
- project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'asd/vim')
- stub_client(repos: [repo], orgs: [], each_page: [OpenStruct.new(objects: [repo])].to_enum)
-
- get :status, format: :json
-
- expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
- expect(json_response.dig("provider_repos")).to eq([])
- end
-
it "touches the etag cache store" do
stub_client(repos: [], orgs: [], each_page: [])
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 b9ae0e23e26..44baadaaade 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,14 +19,4 @@ 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
-
- it "does not show already added project" do
- project = create(:project, import_type: provider_name, namespace: user.namespace, import_status: :finished, import_source: import_source)
- stub_client(client_repos_field => [repo])
-
- get :status, format: :json
-
- expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
- expect(json_response.dig("provider_repos")).to eq([])
- end
end
diff --git a/spec/support/shared_examples/controllers/issuable_anonymous_search_disabled_examples.rb b/spec/support/shared_examples/controllers/issuable_anonymous_search_disabled_examples.rb
new file mode 100644
index 00000000000..e77acb93798
--- /dev/null
+++ b/spec/support/shared_examples/controllers/issuable_anonymous_search_disabled_examples.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'issuable list with anonymous search disabled' do |action|
+ let(:controller_action) { :index }
+ let(:params_with_search) { params.merge(search: 'some search term') }
+
+ context 'when disable_anonymous_search is enabled' do
+ before do
+ stub_feature_flags(disable_anonymous_search: true)
+ end
+
+ it 'shows a flash message' do
+ get controller_action, params: params_with_search
+
+ expect(flash.now[:notice]).to eq('You must sign in to search for specific terms.')
+ end
+
+ context 'when search param is not given' do
+ it 'does not show a flash message' do
+ get controller_action, params: params
+
+ expect(flash.now[:notice]).to be_nil
+ end
+ end
+
+ context 'when user is signed-in' do
+ it 'does not show a flash message' do
+ sign_in(create(:user))
+ get controller_action, params: params_with_search
+
+ expect(flash.now[:notice]).to be_nil
+ end
+ end
+
+ context 'when format is not HTML' do
+ it 'does not show a flash message' do
+ get controller_action, params: params_with_search.merge(format: :atom)
+
+ expect(flash.now[:notice]).to be_nil
+ end
+ end
+ end
+
+ context 'when disable_anonymous_search is disabled' do
+ before do
+ stub_feature_flags(disable_anonymous_search: false)
+ end
+
+ it 'does not show a flash message' do
+ get controller_action, params: params_with_search
+
+ expect(flash.now[:notice]).to be_nil
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/atom/issuable_shared_examples.rb b/spec/support/shared_examples/features/atom/issuable_shared_examples.rb
new file mode 100644
index 00000000000..17993830f37
--- /dev/null
+++ b/spec/support/shared_examples/features/atom/issuable_shared_examples.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples "an authenticated issuable atom feed" do
+ it "renders atom feed with common issuable information" do
+ expect(response_headers['Content-Type'])
+ .to have_content('application/atom+xml')
+ expect(body).to have_selector('author email', text: issuable.author_public_email)
+ expect(body).to have_selector('assignees assignee email', text: issuable.assignees.first.public_email)
+ expect(body).to have_selector('assignee email', text: issuable.assignees.first.public_email)
+ expect(body).to have_selector('entry summary', text: issuable.title)
+ end
+end
diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb
new file mode 100644
index 00000000000..2332285540a
--- /dev/null
+++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'edits content using the content editor' do
+ it 'formats text as bold using bubble menu' do
+ content_editor_testid = '[data-testid="content-editor"] [contenteditable]'
+
+ expect(page).to have_css(content_editor_testid)
+
+ find(content_editor_testid).send_keys 'Typing text in the content editor'
+ find(content_editor_testid).send_keys [:shift, :left]
+
+ expect(page).to have_css('[data-testid="formatting-bubble-menu"]')
+ 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 fd77297a490..e70f9b52c09 100644
--- a/spec/support/shared_examples/features/deploy_token_shared_examples.rb
+++ b/spec/support/shared_examples/features/deploy_token_shared_examples.rb
@@ -1,15 +1,22 @@
# frozen_string_literal: true
RSpec.shared_examples 'a deploy token in settings' do
- it 'view deploy tokens' do
+ it 'view deploy tokens', :js do
+ user.update!(time_display_relative: true)
+
+ visit page_path
+
within('.deploy-tokens') do
expect(page).to have_content(deploy_token.name)
expect(page).to have_content('read_repository')
expect(page).to have_content('read_registry')
+ expect(page).to have_content('in 4 days')
end
end
it 'add a new deploy token' do
+ visit page_path
+
fill_in 'deploy_token_name', with: 'new_deploy_key'
fill_in 'deploy_token_expires_at', with: (Date.today + 1.month).to_s
fill_in 'deploy_token_username', with: 'deployer'
@@ -24,4 +31,18 @@ RSpec.shared_examples 'a deploy token in settings' do
expect(page).to have_selector("input[name='deploy-token'][readonly='readonly']")
end
end
+
+ context 'when User#time_display_relative is false', :js do
+ before do
+ user.update!(time_display_relative: false)
+ end
+
+ it 'shows absolute times for expires_at' do
+ visit page_path
+
+ within('.deploy-tokens') do
+ expect(page).to have_content(deploy_token.expires_at.strftime('%b %d'))
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
index fb2e422559d..318ba67b9e9 100644
--- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb
+++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
@@ -7,7 +7,7 @@ RSpec.shared_examples 'thread comments for commit and snippet' do |resource_name
let(:menu_selector) { "#{dropdown_selector} .dropdown-menu" }
let(:submit_selector) { "#{form_selector} .js-comment-submit-button" }
let(:close_selector) { "#{form_selector} .btn-comment-and-close" }
- let(:comments_selector) { '.timeline > .note.timeline-entry' }
+ let(:comments_selector) { '.timeline > .note.timeline-entry:not(.being-posted)' }
let(:comment) { 'My comment' }
it 'clicking "Comment" will post a comment' do
@@ -187,7 +187,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re
let(:toggle_selector) { "#{dropdown_selector} .dropdown-toggle-split" }
let(:menu_selector) { "#{dropdown_selector} .dropdown-menu" }
let(:close_selector) { "#{form_selector} .btn-comment-and-close" }
- let(:comments_selector) { '.timeline > .note.timeline-entry' }
+ let(:comments_selector) { '.timeline > .note.timeline-entry:not(.being-posted)' }
let(:comment) { 'My comment' }
it 'clicking "Comment" will post a comment' do
@@ -197,6 +197,8 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re
find(submit_button_selector).click
+ wait_for_all_requests
+
expect(page).to have_content(comment)
new_comment = all(comments_selector).last
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 c0cfc27ceaf..149486320ae 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
@@ -15,7 +15,7 @@ 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-event="click_invite_members"]')
+ expect(page).to have_selector('[data-track-action="click_invite_members"]')
expect(page).to have_selector('[data-track-label="edit_assignee"]')
end
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 38bb87eaed2..0161899cb76 100644
--- a/spec/support/shared_examples/features/manage_applications_shared_examples.rb
+++ b/spec/support/shared_examples/features/manage_applications_shared_examples.rb
@@ -9,9 +9,11 @@ RSpec.shared_examples 'manage applications' do
visit new_application_path
expect(page).to have_content 'Add new application'
+ expect(find('#doorkeeper_application_expire_access_tokens')).to be_checked
fill_in :doorkeeper_application_name, with: application_name
fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri
+ uncheck :doorkeeper_application_expire_access_tokens
check :doorkeeper_application_scopes_read_user
click_on 'Save application'
@@ -22,6 +24,8 @@ RSpec.shared_examples 'manage applications' do
click_on 'Edit'
+ expect(find('#doorkeeper_application_expire_access_tokens')).not_to be_checked
+
application_name_changed = "#{application_name} changed"
fill_in :doorkeeper_application_name, with: application_name_changed
diff --git a/spec/support/shared_examples/features/rss_shared_examples.rb b/spec/support/shared_examples/features/rss_shared_examples.rb
index c7c2aeea358..0991de21d8d 100644
--- a/spec/support/shared_examples/features/rss_shared_examples.rb
+++ b/spec/support/shared_examples/features/rss_shared_examples.rb
@@ -25,3 +25,23 @@ RSpec.shared_examples "it has an RSS button without a feed token" do
.to have_css("a:has(.qa-rss-icon):not([href*='feed_token'])") # rubocop:disable QA/SelectorUsage
end
end
+
+RSpec.shared_examples "updates atom feed link" do |type|
+ it "for #{type}" do
+ sign_in(user)
+ visit path
+
+ link = find_link('Subscribe to RSS feed')
+ params = CGI.parse(URI.parse(link[:href]).query)
+ auto_discovery_link = find("link[type='application/atom+xml']", visible: false)
+ auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
+
+ expected = {
+ 'feed_token' => [user.feed_token],
+ 'assignee_id' => [user.id.to_s]
+ }
+
+ expect(params).to include(expected)
+ expect(auto_discovery_params).to include(expected)
+ end
+end
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 9587da0233e..7ced8508a31 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
@@ -136,6 +136,14 @@ RSpec.shared_examples 'User updates wiki page' do
expect(find('textarea#wiki_content').value).to eq('Updated Wiki Content')
end
end
+
+ context 'when using the content editor' do
+ before do
+ click_button 'Use the new editor'
+ end
+
+ it_behaves_like 'edits content using the content editor'
+ end
end
context 'when the page is in a subdir', :js do
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 61feeff57bb..96df5a5f972 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
@@ -157,7 +157,7 @@ RSpec.shared_examples 'User views a wiki page' do
expect(page).to have_link('updated home', href: wiki_page_path(wiki, wiki_page, version_id: commit2, action: :diff))
end
- it 'between the current and the previous version of a page' do
+ it 'between the current and the previous version of a page', :js do
commit = wiki.commit
visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff)
@@ -169,7 +169,7 @@ RSpec.shared_examples 'User views a wiki page' do
expect_diff_links(commit)
end
- it 'between two old versions of a page' do
+ it 'between two old versions of a page', :js do
wiki_page.update(message: 'latest home change', content: 'updated [another link](other-page)') # rubocop:disable Rails/SaveBang:
commit = wiki.commit('HEAD^')
visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff)
@@ -184,7 +184,7 @@ RSpec.shared_examples 'User views a wiki page' do
expect_diff_links(commit)
end
- it 'for the oldest version of a page' do
+ it 'for the oldest version of a page', :js do
commit = wiki.commit('HEAD^')
visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff)
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
new file mode 100644
index 00000000000..6342064beb8
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+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_deployment_frequency_in_vsa feature flag so we have aggregated data
+ ::Dora::DailyMetrics::RefreshWorker.new.perform(environment.id, Time.current.to_date.to_s) if Gitlab.ee?
+ end
+
+ describe "#deploys" do
+ subject { stage_summary.third }
+
+ context 'when from date is given' do
+ before do
+ travel_to(5.days.ago) { create_deployment(project: project) }
+ create_deployment(project: project)
+ end
+
+ it "finds the number of deploys made created after the 'from date'" do
+ expect(subject[:value]).to eq('1')
+ end
+
+ it 'returns the localized title' do
+ Gitlab::I18n.with_locale(:ru) do
+ expect(subject[:title]).to eq(n_('Deploy', 'Deploys', 1))
+ end
+ end
+ end
+
+ it "doesn't find commits from other projects" do
+ travel_to(5.days.from_now) do
+ create_deployment(project: create(:project, :repository))
+ end
+
+ expect(subject[:value]).to eq('-')
+ end
+
+ context 'when `to` parameter is given' do
+ before do
+ travel_to(5.days.ago) { create_deployment(project: project) }
+ travel_to(5.days.from_now) { create_deployment(project: project) }
+ end
+
+ it "doesn't find any record" do
+ options[:to] = Time.now
+
+ expect(subject[:value]).to eq('-')
+ end
+
+ it "finds records created between `from` and `to` range" do
+ options[:from] = 10.days.ago
+ options[:to] = 10.days.from_now
+
+ expect(subject[:value]).to eq('2')
+ end
+ end
+ end
+
+ describe '#deployment_frequency' do
+ subject { stage_summary.fourth[:value] }
+
+ it 'includes the unit: `per day`' do
+ expect(stage_summary.fourth[:unit]).to eq _('per day')
+ end
+
+ before do
+ travel_to(5.days.ago) { create_deployment(project: project) }
+ end
+
+ it 'returns 0.0 when there were deploys but the frequency was too low' do
+ options[:from] = 30.days.ago
+
+ # 1 deployment over 30 days
+ # frequency of 0.03, rounded off to 0.0
+ expect(subject).to eq('0')
+ end
+
+ it 'returns `-` when there were no deploys' do
+ options[:from] = 4.days.ago
+
+ # 0 deployment in the last 4 days
+ expect(subject).to eq('-')
+ end
+
+ context 'when `to` is nil' do
+ it 'includes range until now' do
+ options[:from] = 6.days.ago
+ options[:to] = nil
+
+ # 1 deployment over 7 days
+ expect(subject).to eq('0.1')
+ end
+ end
+
+ context 'when `to` is given' do
+ before do
+ travel_to(5.days.from_now) { create_deployment(project: project, finished_at: Time.zone.now) }
+ end
+
+ it 'finds records created between `from` and `to` range' do
+ options[:from] = 10.days.ago
+ options[:to] = 10.days.from_now
+
+ # 2 deployments over 20 days
+ expect(subject).to eq('0.1')
+ end
+
+ context 'when `from` and `to` are within a day' do
+ it 'returns the number of deployments made on that day' do
+ freeze_time do
+ create_deployment(project: project, finished_at: Time.current)
+ options[:from] = Time.current.yesterday.beginning_of_day
+ options[:to] = Time.current.end_of_day
+
+ expect(subject).to eq('0.5')
+ end
+ end
+ end
+ 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 89b793d5e16..708bc71ae96 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
@@ -39,6 +39,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
allow(fake_duplicate_job).to receive(:scheduled?).and_return(false)
allow(fake_duplicate_job).to receive(:check!).and_return('the jid')
allow(fake_duplicate_job).to receive(:idempotent?).and_return(true)
+ allow(fake_duplicate_job).to receive(:update_latest_wal_location!)
allow(fake_duplicate_job).to receive(:options).and_return({})
job_hash = {}
@@ -63,6 +64,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
.with(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DUPLICATE_KEY_TTL)
.and_return('the jid'))
allow(fake_duplicate_job).to receive(:idempotent?).and_return(true)
+ allow(fake_duplicate_job).to receive(:update_latest_wal_location!)
job_hash = {}
expect(fake_duplicate_job).to receive(:duplicate?).and_return(true)
@@ -83,6 +85,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
allow(fake_duplicate_job).to(
receive(:check!).with(time_diff.to_i).and_return('the jid'))
allow(fake_duplicate_job).to receive(:idempotent?).and_return(true)
+ allow(fake_duplicate_job).to receive(:update_latest_wal_location!)
job_hash = {}
expect(fake_duplicate_job).to receive(:duplicate?).and_return(true)
@@ -105,6 +108,13 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
allow(fake_duplicate_job).to receive(:options).and_return({})
allow(fake_duplicate_job).to receive(:existing_jid).and_return('the jid')
allow(fake_duplicate_job).to receive(:idempotent?).and_return(true)
+ allow(fake_duplicate_job).to receive(:update_latest_wal_location!)
+ end
+
+ it 'updates latest wal location' do
+ expect(fake_duplicate_job).to receive(:update_latest_wal_location!)
+
+ strategy.schedule({ 'jid' => 'new jid' }) {}
end
it 'drops the job' do
@@ -136,4 +146,46 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
end
end
end
+
+ describe '#perform' do
+ let(:proc) { -> {} }
+ let(:job) { { 'jid' => 'new jid', 'wal_locations' => { 'main' => '0/1234', 'ci' => '0/1234' } } }
+ let(:wal_locations) do
+ {
+ main: '0/D525E3A8',
+ ci: 'AB/12345'
+ }
+ end
+
+ before do
+ allow(fake_duplicate_job).to receive(:delete!)
+ allow(fake_duplicate_job).to receive(:latest_wal_locations).and_return( wal_locations )
+ end
+
+ it 'updates job hash with dedup_wal_locations' do
+ strategy.perform(job) do
+ proc.call
+ end
+
+ expect(job['dedup_wal_locations']).to eq(wal_locations)
+ end
+
+ shared_examples 'does not update job hash' do
+ it 'does not update job hash with dedup_wal_locations' do
+ strategy.perform(job) do
+ proc.call
+ end
+
+ expect(job).not_to include('dedup_wal_locations')
+ end
+ end
+
+ context 'when latest_wal_location is empty' do
+ before do
+ allow(fake_duplicate_job).to receive(:latest_wal_locations).and_return( {} )
+ end
+
+ include_examples 'does not update job hash'
+ 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 b10ebb4d2a3..e1f7a9030e2 100644
--- a/spec/support/shared_examples/mailers/notify_shared_examples.rb
+++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb
@@ -2,7 +2,7 @@
RSpec.shared_examples 'a multiple recipients email' do
it 'is sent to the given recipient' do
- is_expected.to deliver_to recipient.notification_email
+ is_expected.to deliver_to recipient.notification_email_or_default
end
end
@@ -21,7 +21,7 @@ end
RSpec.shared_examples 'an email sent to a user' do
it 'is sent to user\'s global notification email address' do
- expect(subject).to deliver_to(recipient.notification_email)
+ expect(subject).to deliver_to(recipient.notification_email_or_default)
end
context 'with group notification email' do
@@ -227,7 +227,7 @@ RSpec.shared_examples 'a note email' do
aggregate_failures do
expect(sender.display_name).to eq("#{note_author.name} (@#{note_author.username})")
expect(sender.address).to eq(gitlab_sender)
- expect(subject).to deliver_to(recipient.notification_email)
+ expect(subject).to deliver_to(recipient.notification_email_or_default)
end
end
diff --git a/spec/support/shared_examples/models/concerns/featurable_shared_examples.rb b/spec/support/shared_examples/models/concerns/featurable_shared_examples.rb
new file mode 100644
index 00000000000..2f165ef604f
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/featurable_shared_examples.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'access level validation' do |features|
+ features.each do |feature|
+ it "does not allow public access level for #{feature}" do
+ field = "#{feature}_access_level".to_sym
+ container_features.update_attribute(field, ProjectFeature::PUBLIC)
+
+ expect(container_features.valid?).to be_falsy, "#{field} failed"
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/sanitizable_shared_examples.rb b/spec/support/shared_examples/models/concerns/sanitizable_shared_examples.rb
new file mode 100644
index 00000000000..ed94a71892d
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/sanitizable_shared_examples.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'sanitizable' do |factory, fields|
+ let(:attributes) { fields.to_h { |field| [field, input] } }
+
+ it 'includes Sanitizable' do
+ expect(described_class).to include(Sanitizable)
+ end
+
+ fields.each do |field|
+ subject do
+ record = build(factory, attributes)
+ record.valid?
+
+ record.public_send(field)
+ end
+
+ describe "##{field}" do
+ context 'when input includes javascript tags' do
+ let(:input) { 'hello<script>alert(1)</script>' }
+
+ it 'gets sanitized' do
+ expect(subject).to eq('hello')
+ end
+ end
+ end
+
+ describe "##{field} validation" do
+ context 'when input contains pre-escaped html entities' do
+ let_it_be(:input) { '&lt;script&gt;alert(1)&lt;/script&gt;' }
+
+ subject { build(factory, attributes) }
+
+ it 'is not valid', :aggregate_failures do
+ expect(subject).not_to be_valid
+ expect(subject.errors.details[field].flat_map(&:values)).to include('cannot contain escaped HTML entities')
+ end
+ end
+ end
+ 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 c111d250d34..56c202cb228 100644
--- a/spec/support/shared_examples/models/member_shared_examples.rb
+++ b/spec/support/shared_examples/models/member_shared_examples.rb
@@ -300,8 +300,21 @@ RSpec.shared_examples_for "member creation" do
end
end
end
+end
+
+RSpec.shared_examples_for "bulk member creation" do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
+
+ describe '#execute' do
+ it 'raises an error when exiting_members is not passed in the args hash' do
+ expect do
+ described_class.new(source, user, :maintainer, current_user: user).execute
+ end.to raise_error(ArgumentError, 'existing_members must be included in the args hash')
+ end
+ end
- describe '.add_users' do
+ describe '.add_users', :aggregate_failures do
let_it_be(:user1) { create(:user) }
let_it_be(:user2) { create(:user) }
@@ -310,8 +323,8 @@ RSpec.shared_examples_for "member creation" do
expect(members).to be_a Array
expect(members.size).to eq(2)
- expect(members.first).to be_a member_type
- expect(members.first).to be_persisted
+ expect(members).to all(be_a(member_type))
+ expect(members).to all(be_persisted)
end
it 'returns an empty array' do
@@ -329,5 +342,42 @@ RSpec.shared_examples_for "member creation" do
expect(members.size).to eq(4)
expect(members.first).to be_invite
end
+
+ context 'with de-duplication' do
+ it 'with the same user by id and user' do
+ members = described_class.add_users(source, [user1.id, user1, user1.id, user2, user2.id, user2], :maintainer)
+
+ expect(members).to be_a Array
+ expect(members.size).to eq(2)
+ expect(members).to all(be_a(member_type))
+ expect(members).to all(be_persisted)
+ end
+
+ it 'with the same user sent more than once' do
+ members = described_class.add_users(source, [user1, user1], :maintainer)
+
+ expect(members).to be_a Array
+ expect(members.size).to eq(1)
+ expect(members).to all(be_a(member_type))
+ expect(members).to all(be_persisted)
+ end
+ end
+
+ context 'when a member already exists' do
+ before do
+ source.add_user(user1, :developer)
+ end
+
+ it 'supports existing users as expected' do
+ user3 = create(:user)
+
+ members = described_class.add_users(source, [user1.id, user2, user3.id], :maintainer)
+
+ expect(members).to be_a Array
+ expect(members.size).to eq(3)
+ expect(members).to all(be_a(member_type))
+ expect(members).to all(be_persisted)
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/models/mentionable_shared_examples.rb b/spec/support/shared_examples/models/mentionable_shared_examples.rb
index 07c5f730e95..e23658d1774 100644
--- a/spec/support/shared_examples/models/mentionable_shared_examples.rb
+++ b/spec/support/shared_examples/models/mentionable_shared_examples.rb
@@ -207,7 +207,7 @@ RSpec.shared_examples 'an editable mentionable' do
end
RSpec.shared_examples 'mentions in description' do |mentionable_type|
- shared_examples 'when storing user mentions' do
+ context 'when storing user mentions' do
before do
mentionable.store_mentions!
end
@@ -238,26 +238,10 @@ RSpec.shared_examples 'mentions in description' do |mentionable_type|
end
end
end
-
- context 'when store_mentions_without_subtransaction is enabled' do
- before do
- stub_feature_flags(store_mentions_without_subtransaction: true)
- end
-
- it_behaves_like 'when storing user mentions'
- end
-
- context 'when store_mentions_without_subtransaction is disabled' do
- before do
- stub_feature_flags(store_mentions_without_subtransaction: false)
- end
-
- it_behaves_like 'when storing user mentions'
- end
end
RSpec.shared_examples 'mentions in notes' do |mentionable_type|
- shared_examples 'when mentionable notes contain mentions' do
+ context 'when mentionable notes contain mentions' do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:group) { create(:group) }
@@ -277,22 +261,6 @@ RSpec.shared_examples 'mentions in notes' do |mentionable_type|
expect(mentionable.referenced_groups(user)).to eq [group]
end
end
-
- context 'when store_mentions_without_subtransaction is enabled' do
- before do
- stub_feature_flags(store_mentions_without_subtransaction: true)
- end
-
- it_behaves_like 'when mentionable notes contain mentions'
- end
-
- context 'when store_mentions_without_subtransaction is disabled' do
- before do
- stub_feature_flags(store_mentions_without_subtransaction: false)
- end
-
- it_behaves_like 'when mentionable notes contain mentions'
- end
end
RSpec.shared_examples 'load mentions from DB' do |mentionable_type|
diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
index 4d328c03641..74b1bacc560 100644
--- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
+++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
@@ -31,6 +31,131 @@ RSpec.shared_examples 'namespace traversal scopes' do
it { expect(subject.where_values_hash).not_to have_key(:type) }
end
+ describe '.order_by_depth' do
+ subject { described_class.where(id: [group_1, nested_group_1, deep_nested_group_1]).order_by_depth(direction) }
+
+ context 'ascending' do
+ let(:direction) { :asc }
+
+ it { is_expected.to eq [deep_nested_group_1, nested_group_1, group_1] }
+ end
+
+ context 'descending' do
+ let(:direction) { :desc }
+
+ it { is_expected.to eq [group_1, nested_group_1, deep_nested_group_1] }
+ end
+ end
+
+ describe '.normal_select' do
+ let(:query_result) { described_class.where(id: group_1).normal_select }
+
+ subject { query_result.column_names }
+
+ it { is_expected.to eq described_class.column_names }
+ end
+
+ shared_examples '.self_and_ancestors' do
+ subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_ancestors }
+
+ it { is_expected.to contain_exactly(group_1, nested_group_1, group_2, nested_group_2) }
+
+ context 'when include_self is false' do
+ subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_ancestors(include_self: false) }
+
+ it { is_expected.to contain_exactly(group_1, group_2) }
+ end
+
+ context 'when hierarchy_order is ascending' do
+ subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_ancestors(hierarchy_order: :asc) }
+
+ # Recursive order per level is not defined.
+ it { is_expected.to contain_exactly(nested_group_1, nested_group_2, group_1, group_2) }
+ it { expect(subject[0, 2]).to contain_exactly(nested_group_1, nested_group_2) }
+ it { expect(subject[2, 2]).to contain_exactly(group_1, group_2) }
+ end
+
+ context 'when hierarchy_order is descending' do
+ subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_ancestors(hierarchy_order: :desc) }
+
+ # Recursive order per level is not defined.
+ it { is_expected.to contain_exactly(nested_group_1, nested_group_2, group_1, group_2) }
+ it { expect(subject[0, 2]).to contain_exactly(group_1, group_2) }
+ it { expect(subject[2, 2]).to contain_exactly(nested_group_1, nested_group_2) }
+ end
+ end
+
+ describe '.self_and_ancestors' do
+ context "use_traversal_ids_ancestor_scopes feature flag is true" do
+ before do
+ stub_feature_flags(use_traversal_ids: true)
+ stub_feature_flags(use_traversal_ids_for_ancestor_scopes: true)
+ end
+
+ it_behaves_like '.self_and_ancestors'
+
+ it 'not make recursive queries' do
+ expect { described_class.where(id: [nested_group_1]).self_and_ancestors.load }.not_to make_queries_matching(/WITH RECURSIVE/)
+ end
+ end
+
+ context "use_traversal_ids_ancestor_scopes feature flag is false" do
+ before do
+ stub_feature_flags(use_traversal_ids_for_ancestor_scopes: false)
+ end
+
+ it_behaves_like '.self_and_ancestors'
+
+ it 'make recursive queries' do
+ expect { described_class.where(id: [nested_group_1]).self_and_ancestors.load }.to make_queries_matching(/WITH RECURSIVE/)
+ end
+ end
+ end
+
+ shared_examples '.self_and_ancestor_ids' do
+ subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_ancestor_ids.pluck(:id) }
+
+ it { is_expected.to contain_exactly(group_1.id, nested_group_1.id, group_2.id, nested_group_2.id) }
+
+ context 'when include_self is false' do
+ subject do
+ described_class
+ .where(id: [nested_group_1, nested_group_2])
+ .self_and_ancestor_ids(include_self: false)
+ .pluck(:id)
+ end
+
+ it { is_expected.to contain_exactly(group_1.id, group_2.id) }
+ end
+ end
+
+ describe '.self_and_ancestor_ids' do
+ context "use_traversal_ids_ancestor_scopes feature flag is true" do
+ before do
+ stub_feature_flags(use_traversal_ids: true)
+ stub_feature_flags(use_traversal_ids_for_ancestor_scopes: true)
+ end
+
+ it_behaves_like '.self_and_ancestor_ids'
+
+ it 'make recursive queries' do
+ expect { described_class.where(id: [nested_group_1]).self_and_ancestor_ids.load }.not_to make_queries_matching(/WITH RECURSIVE/)
+ end
+ end
+
+ context "use_traversal_ids_ancestor_scopes feature flag is false" do
+ before do
+ stub_feature_flags(use_traversal_ids_for_ancestor_scopes: false)
+ end
+
+ it_behaves_like '.self_and_ancestor_ids'
+
+ it 'make recursive queries' do
+ expect { described_class.where(id: [nested_group_1]).self_and_ancestor_ids.load }.to make_queries_matching(/WITH RECURSIVE/)
+ end
+ end
+ end
+
describe '.self_and_descendants' do
subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_descendants }
diff --git a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb
index 1ad38a17f9c..acbcf4f7f3d 100644
--- a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb
@@ -36,8 +36,8 @@ RSpec.shared_examples 'process helm service index request' do |user_type, status
expect(yaml_response.keys).to contain_exactly('apiVersion', 'entries', 'generated', 'serverInfo')
expect(yaml_response['entries']).to be_a(Hash)
- expect(yaml_response['entries'].keys).to contain_exactly(package.name)
- expect(yaml_response['serverInfo']).to eq({ 'contextPath' => "/api/v4/projects/#{project.id}/packages/helm" })
+ expect(yaml_response['entries'].keys).to contain_exactly(package.name, package2.name)
+ expect(yaml_response['serverInfo']).to eq({ 'contextPath' => "/api/v4/projects/#{project_id}/packages/helm" })
package_entry = yaml_response['entries'][package.name]
@@ -45,6 +45,14 @@ RSpec.shared_examples 'process helm service index request' do |user_type, status
expect(package_entry.first.keys).to contain_exactly('name', 'version', 'apiVersion', 'created', 'digest', 'urls')
expect(package_entry.first['digest']).to eq('fd2b2fa0329e80a2a602c2bb3b40608bcd6ee5cf96cf46fd0d2800a4c129c9db')
expect(package_entry.first['urls']).to eq(["charts/#{package.name}-#{package.version}.tgz"])
+
+ package_entry = yaml_response['entries'][package2.name]
+
+ expect(package_entry.length).to eq(1)
+ expect(package_entry.first.keys).to contain_exactly('name', 'version', 'apiVersion', 'created', 'digest', 'urls', 'description')
+ expect(package_entry.first['digest']).to eq('file2')
+ expect(package_entry.first['description']).to eq('hello from stable channel')
+ expect(package_entry.first['urls']).to eq(['charts/filename2.tgz'])
end
end
end
@@ -174,6 +182,13 @@ RSpec.shared_examples 'process helm download content request' do |user_type, sta
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if user_type != :anonymous && user_type != :not_a_member
+
+ expect_next_found_instance_of(::Packages::PackageFile) do |package_file|
+ expect(package_file).to receive(:file).and_wrap_original do |m, *args|
+ expect(package_file.id).to eq(package_file2.id)
+ m.call(*args)
+ end
+ end
end
it_behaves_like 'a package tracking event', 'API::HelmPackages', 'pull_package'
@@ -189,7 +204,7 @@ end
RSpec.shared_examples 'rejects helm access with unknown project id' do
context 'with an unknown project' do
- let(:project) { OpenStruct.new(id: 1234567890) }
+ let(:project_id) { 1234567890 }
context 'as anonymous' do
it_behaves_like 'rejects helm packages access', :anonymous, :unauthorized
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 0390e60747f..2af7b616659 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
@@ -21,11 +21,24 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
expect(response).to match_response_schema('public_api/v4/packages/npm_package')
expect(json_response['name']).to eq(package.name)
expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version')
- ::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type|
+ ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any
end
expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags')
end
+
+ it 'avoids N+1 database queries' do
+ control = ActiveRecord::QueryRecorder.new { get(url, headers: headers) }
+
+ create_list(:npm_package, 5, project: project, name: package_name).each do |npm_package|
+ ::Packages::DependencyLink.dependency_types.keys.each do |dependency_type|
+ create(:packages_dependency_link, package: package, dependency_type: dependency_type)
+ end
+ end
+
+ # query count can slightly change between the examples so we're using a custom threshold
+ expect { get(url, headers: headers) }.not_to exceed_query_limit(control).with_threshold(4)
+ end
end
shared_examples 'reject metadata request' do |status:|
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 ecde4ee8565..eb650b7a09f 100644
--- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
@@ -153,3 +153,15 @@ RSpec.shared_examples 'a package tracking event' do |category, action|
expect_snowplow_event(category: category, action: action, **snowplow_gitlab_standard_context)
end
end
+
+RSpec.shared_examples 'not a package tracking event' do
+ before do
+ stub_feature_flags(collect_package_events: true)
+ end
+
+ it 'does not create a gitlab tracking event', :snowplow, :aggregate_failures do
+ expect { subject }.not_to change { Packages::Event.count }
+
+ expect_no_snowplow_event
+ 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 95817624658..2a19ff6f590 100644
--- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
+++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#
# Requires let variables:
-# * throttle_setting_prefix: "throttle_authenticated_api", "throttle_authenticated_web", "throttle_protected_paths", "throttle_authenticated_packages_api"
+# * throttle_setting_prefix: "throttle_authenticated_api", "throttle_authenticated_web", "throttle_protected_paths", "throttle_authenticated_packages_api", "throttle_authenticated_git_lfs", "throttle_authenticated_files_api"
# * request_method
# * request_args
# * other_user_request_args
@@ -14,7 +14,9 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do
"throttle_protected_paths" => "throttle_authenticated_protected_paths_api",
"throttle_authenticated_api" => "throttle_authenticated_api",
"throttle_authenticated_web" => "throttle_authenticated_web",
- "throttle_authenticated_packages_api" => "throttle_authenticated_packages_api"
+ "throttle_authenticated_packages_api" => "throttle_authenticated_packages_api",
+ "throttle_authenticated_git_lfs" => "throttle_authenticated_git_lfs",
+ "throttle_authenticated_files_api" => "throttle_authenticated_files_api"
}
end
@@ -165,7 +167,7 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do
end
# Requires let variables:
-# * throttle_setting_prefix: "throttle_authenticated_web" or "throttle_protected_paths"
+# * throttle_setting_prefix: "throttle_authenticated_web", "throttle_protected_paths", "throttle_authenticated_git_lfs"
# * user
# * url_that_requires_authentication
# * request_method
@@ -176,7 +178,8 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do
let(:throttle_types) do
{
"throttle_protected_paths" => "throttle_authenticated_protected_paths_web",
- "throttle_authenticated_web" => "throttle_authenticated_web"
+ "throttle_authenticated_web" => "throttle_authenticated_web",
+ "throttle_authenticated_git_lfs" => "throttle_authenticated_git_lfs"
}
end
@@ -385,3 +388,194 @@ RSpec.shared_examples 'tracking when dry-run mode is set' do
end
end
end
+
+# Requires let variables:
+# * throttle_name: "throttle_unauthenticated_api", "throttle_unauthenticated_web"
+# * throttle_setting_prefix: "throttle_unauthenticated_api", "throttle_unauthenticated"
+# * url_that_does_not_require_authentication
+# * url_that_is_not_matched
+# * requests_per_period
+# * period_in_seconds
+# * period
+RSpec.shared_examples 'rate-limited unauthenticated requests' do
+ before do
+ # Set low limits
+ settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
+ settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
+ end
+
+ context 'when the throttle is enabled' do
+ before do
+ settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'rejects requests over the rate limit' do
+ # At first, allow requests under the rate limit.
+ requests_per_period.times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ # the last straw
+ expect_rejection { get url_that_does_not_require_authentication }
+ end
+
+ context 'with custom response text' do
+ before do
+ stub_application_setting(rate_limiting_response_text: 'Custom response')
+ end
+
+ it 'rejects requests over the rate limit' do
+ # At first, allow requests under the rate limit.
+ requests_per_period.times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ # the last straw
+ expect_rejection { get url_that_does_not_require_authentication }
+ expect(response.body).to eq("Custom response\n")
+ end
+ end
+
+ it 'allows requests after throttling and then waiting for the next period' do
+ requests_per_period.times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ expect_rejection { get url_that_does_not_require_authentication }
+
+ travel_to(period.from_now) do
+ requests_per_period.times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ expect_rejection { get url_that_does_not_require_authentication }
+ end
+ end
+
+ it 'counts requests from different IPs separately' do
+ requests_per_period.times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ expect_next_instance_of(Rack::Attack::Request) do |instance|
+ expect(instance).to receive(:ip).at_least(:once).and_return('1.2.3.4')
+ end
+
+ # would be over limit for the same IP
+ get url_that_does_not_require_authentication
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ context 'when the request is not matched by the throttle' do
+ it 'does not throttle the requests' do
+ (1 + requests_per_period).times do
+ get url_that_is_not_matched
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
+ context 'when the request is to the api internal endpoints' do
+ it 'allows requests over the rate limit' do
+ (1 + requests_per_period).times do
+ get '/api/v4/internal/check', params: { secret_token: Gitlab::Shell.secret_token }
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
+ context 'when the request is authenticated by a runner token' do
+ let(:request_jobs_url) { '/api/v4/jobs/request' }
+ let(:runner) { create(:ci_runner) }
+
+ it 'does not count as unauthenticated' do
+ (1 + requests_per_period).times do
+ post request_jobs_url, params: { token: runner.token }
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+ end
+
+ context 'when the request is to a health endpoint' do
+ let(:health_endpoint) { '/-/metrics' }
+
+ it 'does not throttle the requests' do
+ (1 + requests_per_period).times do
+ get health_endpoint
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
+ context 'when the request is to a container registry notification endpoint' do
+ let(:secret_token) { 'secret_token' }
+ let(:events) { [{ action: 'push' }] }
+ let(:registry_endpoint) { '/api/v4/container_registry_event/events' }
+ let(:registry_headers) { { 'Content-Type' => ::API::ContainerRegistryEvent::DOCKER_DISTRIBUTION_EVENTS_V1_JSON } }
+
+ before do
+ allow(Gitlab.config.registry).to receive(:notification_secret) { secret_token }
+
+ event = spy(:event)
+ allow(::ContainerRegistry::Event).to receive(:new).and_return(event)
+ allow(event).to receive(:supported?).and_return(true)
+ end
+
+ 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)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
+ it 'logs RackAttack info into structured logs' do
+ requests_per_period.times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ arguments = a_hash_including({
+ message: 'Rack_Attack',
+ env: :throttle,
+ remote_ip: '127.0.0.1',
+ request_method: 'GET',
+ path: url_that_does_not_require_authentication,
+ matched: throttle_name
+ })
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(arguments)
+
+ get url_that_does_not_require_authentication
+ end
+
+ it_behaves_like 'tracking when dry-run mode is set' do
+ def do_request
+ get url_that_does_not_require_authentication
+ end
+ end
+ end
+
+ context 'when the throttle is disabled' do
+ before do
+ settings_to_set[:"#{throttle_setting_prefix}_enabled"] = false
+ stub_application_setting(settings_to_set)
+ end
+
+ it 'allows requests over the rate limit' do
+ (1 + requests_per_period).times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/dependency_proxy_ttl_policies_shared_examples.rb b/spec/support/shared_examples/services/dependency_proxy_ttl_policies_shared_examples.rb
new file mode 100644
index 00000000000..f6692646ca8
--- /dev/null
+++ b/spec/support/shared_examples/services/dependency_proxy_ttl_policies_shared_examples.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'updating the dependency proxy image ttl policy attributes' do |from: {}, to:|
+ it_behaves_like 'not creating the dependency proxy image ttl policy'
+
+ it 'updates the dependency proxy image ttl policy' do
+ expect { subject }
+ .to change { group.dependency_proxy_image_ttl_policy.reload.enabled }.from(from[:enabled]).to(to[:enabled])
+ .and change { group.dependency_proxy_image_ttl_policy.reload.ttl }.from(from[:ttl]).to(to[:ttl])
+ end
+end
+
+RSpec.shared_examples 'not creating the dependency proxy image ttl policy' do
+ it "doesn't create the dependency proxy image ttl policy" do
+ expect { subject }.not_to change { DependencyProxy::ImageTtlGroupPolicy.count }
+ end
+end
+
+RSpec.shared_examples 'creating the dependency proxy image ttl policy' do
+ it 'creates a new package setting' do
+ expect { subject }.to change { DependencyProxy::ImageTtlGroupPolicy.count }.by(1)
+ end
+
+ it 'saves the settings' do
+ subject
+
+ expect(group.dependency_proxy_image_ttl_policy).to have_attributes(
+ enabled: ttl_policy[:enabled],
+ ttl: ttl_policy[:ttl]
+ )
+ end
+
+ it_behaves_like 'returning a success'
+end
diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb
index 9fced12b543..0277cce975a 100644
--- a/spec/support/shared_examples/services/incident_shared_examples.rb
+++ b/spec/support/shared_examples/services/incident_shared_examples.rb
@@ -13,6 +13,7 @@
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
@@ -41,6 +42,7 @@ 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
it 'has not an incident label' do
diff --git a/spec/support/shared_examples/services/users/dismiss_user_callout_service_shared_examples.rb b/spec/support/shared_examples/services/users/dismiss_user_callout_service_shared_examples.rb
new file mode 100644
index 00000000000..09820593cdb
--- /dev/null
+++ b/spec/support/shared_examples/services/users/dismiss_user_callout_service_shared_examples.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples_for 'dismissing user callout' do |model|
+ it 'creates a new user callout' do
+ expect { execute }.to change { model.count }.by(1)
+ end
+
+ it 'returns a user callout' do
+ expect(execute).to be_an_instance_of(model)
+ end
+
+ it 'sets the dismissed_at attribute to current time' do
+ freeze_time do
+ expect(execute).to have_attributes(dismissed_at: Time.current)
+ end
+ end
+
+ it 'updates an existing callout dismissed_at time' do
+ freeze_time do
+ old_time = 1.day.ago
+ new_time = Time.current
+ attributes = params.merge(dismissed_at: old_time, user: user)
+ existing_callout = create("#{model.name.split('::').last.underscore}".to_sym, attributes)
+
+ expect { execute }.to change { existing_callout.reload.dismissed_at }.from(old_time).to(new_time)
+ end
+ end
+
+ it 'does not update an invalid record with dismissed_at time', :aggregate_failures do
+ callout = described_class.new(
+ container: nil, current_user: user, params: { feature_name: nil }
+ ).execute
+
+ expect(callout.dismissed_at).to be_nil
+ expect(callout).to be_invalid
+ end
+end
diff --git a/spec/support/shared_examples/work_item_base_types_importer.rb b/spec/support/shared_examples/work_item_base_types_importer.rb
new file mode 100644
index 00000000000..7d652be8d05
--- /dev/null
+++ b/spec/support/shared_examples/work_item_base_types_importer.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'work item base types importer' do
+ it 'creates all base work item types' do
+ # Fixtures need to run on a pristine DB, but the test suite preloads the base types before(:suite)
+ WorkItem::Type.delete_all
+
+ expect { subject }.to change(WorkItem::Type, :count).from(0).to(WorkItem::Type::BASE_TYPES.count)
+ end
+end