diff options
Diffstat (limited to 'spec/support/shared_examples')
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) { '<script>alert(1)</script>' } + + 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 |