diff options
Diffstat (limited to 'spec/support/shared_examples')
77 files changed, 3451 insertions, 324 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 113252a6ab5..84910d0dfe4 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 @@ -85,7 +85,7 @@ RSpec.shared_examples 'multiple issue boards' do wait_for_requests - expect(page).to have_selector('.board', count: 3) + expect(page).to have_selector('.board', count: 5) in_boards_switcher_dropdown do click_link board.name @@ -93,7 +93,7 @@ RSpec.shared_examples 'multiple issue boards' do wait_for_requests - expect(page).to have_selector('.board', count: 2) + expect(page).to have_selector('.board', count: 4) end it 'maintains sidebar state over board switch' do diff --git a/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb b/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb new file mode 100644 index 00000000000..54d41f9a68c --- /dev/null +++ b/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'project access tokens available #index' do + let_it_be(:active_project_access_token) { create(:personal_access_token, user: bot_user) } + let_it_be(:inactive_project_access_token) { create(:personal_access_token, :revoked, user: bot_user) } + + it 'retrieves active project access tokens' do + subject + + expect(assigns(:active_project_access_tokens)).to contain_exactly(active_project_access_token) + end + + it 'retrieves inactive project access tokens' do + subject + + expect(assigns(:inactive_project_access_tokens)).to contain_exactly(inactive_project_access_token) + end + + it 'lists all available scopes' do + subject + + expect(assigns(:scopes)).to eq(Gitlab::Auth.resource_bot_scopes) + end + + it 'retrieves newly created personal access token value' do + token_value = 'random-value' + allow(PersonalAccessToken).to receive(:redis_getdel).with("#{user.id}:#{project.id}").and_return(token_value) + + subject + + expect(assigns(:new_project_access_token)).to eq(token_value) + end +end + +RSpec.shared_examples 'project access tokens available #create' do + def created_token + PersonalAccessToken.order(:created_at).last + end + + it 'returns success message' do + subject + + expect(response.flash[:notice]).to match('Your new project access token has been created.') + end + + it 'creates project access token' do + subject + + expect(created_token.name).to eq(access_token_params[:name]) + expect(created_token.scopes).to eq(access_token_params[:scopes]) + expect(created_token.expires_at).to eq(access_token_params[:expires_at]) + end + + it 'creates project bot user' do + subject + + expect(created_token.user).to be_project_bot + end + + it 'stores newly created token redis store' do + expect(PersonalAccessToken).to receive(:redis_store!) + + subject + end + + it { expect { subject }.to change { User.count }.by(1) } + it { expect { subject }.to change { PersonalAccessToken.count }.by(1) } + + context 'when unsuccessful' do + before do + allow_next_instance_of(ResourceAccessTokens::CreateService) do |service| + allow(service).to receive(:execute).and_return ServiceResponse.error(message: 'Failed!') + end + end + + it { expect(subject).to render_template(:index) } + end +end + +RSpec.shared_examples 'project access tokens available #revoke' do + it 'calls delete user worker' do + expect(DeleteUserWorker).to receive(:perform_async).with(user.id, bot_user.id, skip_authorization: true) + + subject + end + + it 'removes membership of bot user' do + subject + + expect(project.reload.bots).not_to include(bot_user) + end + + it 'converts issuables of the bot user to ghost user' do + issue = create(:issue, author: bot_user) + + subject + + expect(issue.reload.author.ghost?).to be true + end + + it 'deletes project bot user' do + subject + + expect(User.exists?(bot_user.id)).to be_falsy + end +end diff --git a/spec/support/shared_examples/controllers/cache_control_shared_examples.rb b/spec/support/shared_examples/controllers/cache_control_shared_examples.rb index 426d7f95222..5496e04e26c 100644 --- a/spec/support/shared_examples/controllers/cache_control_shared_examples.rb +++ b/spec/support/shared_examples/controllers/cache_control_shared_examples.rb @@ -2,7 +2,7 @@ RSpec.shared_examples 'project cache control headers' do before do - project.update(visibility_level: visibility_level) + project.update!(visibility_level: visibility_level) end context 'when project is public' do diff --git a/spec/support/shared_examples/controllers/destroy_hook_shared_examples.rb b/spec/support/shared_examples/controllers/destroy_hook_shared_examples.rb new file mode 100644 index 00000000000..710aa333dec --- /dev/null +++ b/spec/support/shared_examples/controllers/destroy_hook_shared_examples.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'Web hook destroyer' do + it 'displays a message about synchronous delete', :aggregate_failures do + expect_next_instance_of(WebHooks::DestroyService) do |instance| + expect(instance).to receive(:execute).with(anything).and_call_original + end + + delete :destroy, params: params + + expect(response).to have_gitlab_http_status(:found) + expect(flash[:notice]).to eq("#{hook.model_name.human} was deleted") + end + + it 'displays a message about async delete', :aggregate_failures do + expect_next_instance_of(WebHooks::DestroyService) do |instance| + expect(instance).to receive(:execute).with(anything).and_return({ status: :success, async: true } ) + end + + delete :destroy, params: params + + expect(response).to have_gitlab_http_status(:found) + expect(flash[:notice]).to eq("#{hook.model_name.human} was scheduled for deletion") + end + + it 'displays an error if deletion failed', :aggregate_failures do + expect_next_instance_of(WebHooks::DestroyService) do |instance| + expect(instance).to receive(:execute).with(anything).and_return({ status: :error, async: true, message: "failed" } ) + end + + delete :destroy, params: params + + expect(response).to have_gitlab_http_status(:found) + expect(flash[:alert]).to eq("failed") + 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 8bc91f72b8c..2fcc88ef36a 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 @@ -262,7 +262,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do context "when the namespace is owned by the GitLab user" do before do user.username = other_username - user.save + user.save! end it "takes the existing namespace" do diff --git a/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb index 7f26155f9d6..3f147f942ba 100644 --- a/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb +++ b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb @@ -59,7 +59,7 @@ RSpec.shared_examples 'known sign in' do it 'notifies the user when the cookie is expired' do stub_cookie - Timecop.freeze((KnownSignIn::KNOWN_SIGN_IN_COOKIE_EXPIRY + 1.day).from_now) do + travel_to((KnownSignIn::KNOWN_SIGN_IN_COOKIE_EXPIRY + 1.day).from_now) do expect_next_instance_of(NotificationService) do |instance| expect(instance).to receive(:unknown_sign_in) end diff --git a/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb b/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb index 925c45005f0..2d35b1681ea 100644 --- a/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb +++ b/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb @@ -2,9 +2,28 @@ RSpec.shared_examples 'milestone tabs' do def go(path, extra_params = {}) - params = { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid } + get path, params: request_params.merge(extra_params) + end + + describe '#issues' do + context 'as html' do + before do + go(:issues, format: 'html') + end - get path, params: params.merge(extra_params) + it 'redirects to milestone#show' do + expect(response).to redirect_to(milestone_path) + end + end + + context 'as json' do + it 'renders the issues tab template to a string' do + go(:issues, format: 'json') + + expect(response).to render_template('shared/milestones/_issues_tab') + expect(json_response).to have_key('html') + end + end end describe '#merge_requests' do diff --git a/spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb b/spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb index f2a97a86df6..b67eb0d99fd 100644 --- a/spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb @@ -44,7 +44,7 @@ RSpec.shared_examples 'authenticates sessionless user' do |path, format, params| .to increment(:user_unauthenticated_counter) end - personal_access_token.update(scopes: [:read_user]) + personal_access_token.update!(scopes: [:read_user]) get path, params: default_params.merge(private_token: personal_access_token.token) diff --git a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb index 7e5a225f020..cf7ee17ea13 100644 --- a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb +++ b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb @@ -1,47 +1,24 @@ # frozen_string_literal: true +# +# Requires a context containing: +# - request +# - expected_type +# - target_id RSpec.shared_examples 'tracking unique hll events' do |feature_flag| - context 'when format is HTML' do - let(:format) { :html } + it 'tracks unique event' do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(expected_type, target_id) - it 'tracks unique event' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(expected_type, target_id) - - subject - end - - it 'tracks unique event if DNT is not enabled' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(expected_type, target_id) - request.headers['DNT'] = '0' - - subject - end - - it 'does not track unique event if DNT is enabled' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event).with(expected_type, target_id) - request.headers['DNT'] = '1' - - subject - end - - context 'when feature flag is disabled' do - it 'does not track unique event' do - stub_feature_flags(feature_flag => false) - - expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event).with(expected_type, target_id) - - subject - end - end + request end - context 'when format is JSON' do - let(:format) { :json } + context 'when feature flag is disabled' do + it 'does not track unique event' do + stub_feature_flags(feature_flag => false) - it 'does not track unique event if the format is JSON' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event).with(expected_type, target_id) + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) - subject + request end end end diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb index 4ca400dd87b..a6ad8fc594c 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -15,10 +15,10 @@ RSpec.shared_examples 'wiki controller actions' do end describe 'GET #new' do - subject { get :new, params: routing_params } + subject(:request) { get :new, params: routing_params } it 'redirects to #show and appends a `random_title` param' do - subject + request expect(response).to be_redirect expect(response.redirect_url).to match(%r{ @@ -35,7 +35,7 @@ RSpec.shared_examples 'wiki controller actions' do end it 'redirects to the wiki container and displays an error message' do - subject + request expect(response).to redirect_to(container) expect(flash[:notice]).to eq('Could not create Wiki Repository at this time. Please try again later.') @@ -146,13 +146,13 @@ RSpec.shared_examples 'wiki controller actions' do let(:random_title) { nil } - subject { get :show, params: routing_params.merge(id: id, random_title: random_title) } + subject(:request) { get :show, params: routing_params.merge(id: id, random_title: random_title) } context 'when page exists' do let(:id) { wiki_title } it 'renders the page' do - subject + request expect(response).to have_gitlab_http_status(:ok) expect(response).to render_template('shared/wikis/show') @@ -161,19 +161,26 @@ RSpec.shared_examples 'wiki controller actions' do expect(assigns(:sidebar_limited)).to be(false) end - it 'increases the page view counter' do - expect do - subject + context 'page view tracking' do + it_behaves_like 'tracking unique hll events', :track_unique_wiki_page_views do + let(:target_id) { 'wiki_action' } + let(:expected_type) { instance_of(String) } + end - expect(response).to have_gitlab_http_status(:ok) - end.to change { Gitlab::UsageDataCounters::WikiPageCounter.read(:view) }.by(1) + it 'increases the page view counter' do + expect do + request + + expect(response).to have_gitlab_http_status(:ok) + end.to change { Gitlab::UsageDataCounters::WikiPageCounter.read(:view) }.by(1) + end end context 'when page content encoding is invalid' do it 'sets flash error' do allow(controller).to receive(:valid_encoding?).and_return(false) - subject + request expect(response).to have_gitlab_http_status(:ok) expect(response).to render_template('shared/wikis/show') @@ -187,7 +194,7 @@ RSpec.shared_examples 'wiki controller actions' do context 'when the user can create pages' do before do - subject + request expect(response).to have_gitlab_http_status(:ok) expect(response).to render_template('shared/wikis/edit') @@ -212,7 +219,7 @@ RSpec.shared_examples 'wiki controller actions' do end it 'shows the empty state' do - subject + request expect(response).to have_gitlab_http_status(:ok) expect(response).to render_template('shared/wikis/empty') @@ -226,10 +233,10 @@ RSpec.shared_examples 'wiki controller actions' do where(:file_name) { ['dk.png', 'unsanitized.svg', 'git-cheat-sheet.pdf'] } with_them do - let(:id) { upload_file_to_wiki(container, user, file_name) } + let(:id) { upload_file_to_wiki(wiki, user, file_name) } it 'delivers the file with the correct headers' do - subject + request expect(response).to have_gitlab_http_status(:ok) expect(response.headers['Content-Disposition']).to match(/^inline/) @@ -255,7 +262,7 @@ RSpec.shared_examples 'wiki controller actions' do let(:id_param) { 'invalid' } it 'redirects to show' do - subject + request expect(response).to redirect_to_wiki(wiki, 'invalid') end @@ -265,7 +272,7 @@ RSpec.shared_examples 'wiki controller actions' do let(:id_param) { ' ' } it 'redirects to the home page' do - subject + request expect(response).to redirect_to_wiki(wiki, 'home') end @@ -275,7 +282,7 @@ RSpec.shared_examples 'wiki controller actions' do it 'redirects to show' do allow(controller).to receive(:valid_encoding?).and_return(false) - subject + request expect(response).to redirect_to_wiki(wiki, wiki.list_pages.first) end @@ -288,7 +295,7 @@ RSpec.shared_examples 'wiki controller actions' do allow(page).to receive(:content).and_return(nil) allow(controller).to receive(:page).and_return(page) - subject + request expect(response).to redirect_to_wiki(wiki, page) end @@ -298,7 +305,7 @@ RSpec.shared_examples 'wiki controller actions' do describe 'GET #edit' do let(:id_param) { wiki_title } - subject { get(:edit, params: routing_params.merge(id: id_param)) } + subject(:request) { get(:edit, params: routing_params.merge(id: id_param)) } it_behaves_like 'edit action' @@ -306,7 +313,7 @@ RSpec.shared_examples 'wiki controller actions' do render_views it 'shows the edit page' do - subject + request expect(response).to have_gitlab_http_status(:ok) expect(response.body).to include(s_('Wiki|Edit Page')) @@ -319,7 +326,7 @@ RSpec.shared_examples 'wiki controller actions' do let(:new_content) { 'New content' } let(:id_param) { wiki_title } - subject do + subject(:request) do patch(:update, params: routing_params.merge( id: id_param, @@ -333,7 +340,7 @@ RSpec.shared_examples 'wiki controller actions' do render_views it 'updates the page' do - subject + request wiki_page = wiki.list_pages(load_content: true).first @@ -348,7 +355,7 @@ RSpec.shared_examples 'wiki controller actions' do end it 'renders the empty state' do - subject + request expect(response).to render_template('shared/wikis/empty') end @@ -359,7 +366,7 @@ RSpec.shared_examples 'wiki controller actions' do let(:new_title) { 'New title' } let(:new_content) { 'New content' } - subject do + subject(:request) do post(:create, params: routing_params.merge( wiki: { title: new_title, content: new_content } @@ -369,7 +376,7 @@ RSpec.shared_examples 'wiki controller actions' do context 'when page is valid' do it 'creates the page' do expect do - subject + request end.to change { wiki.list_pages.size }.by 1 wiki_page = wiki.find_page(new_title) @@ -384,7 +391,7 @@ RSpec.shared_examples 'wiki controller actions' do it 'renders the edit state' do expect do - subject + request end.not_to change { wiki.list_pages.size } expect(response).to render_template('shared/wikis/edit') @@ -395,7 +402,7 @@ RSpec.shared_examples 'wiki controller actions' do describe 'DELETE #destroy' do let(:id_param) { wiki_title } - subject do + subject(:request) do delete(:destroy, params: routing_params.merge( id: id_param @@ -405,7 +412,7 @@ RSpec.shared_examples 'wiki controller actions' do context 'when page exists' do it 'deletes the page' do expect do - subject + request end.to change { wiki.list_pages.size }.by(-1) end @@ -418,7 +425,7 @@ RSpec.shared_examples 'wiki controller actions' do it 'renders the edit state' do expect do - subject + request end.not_to change { wiki.list_pages.size } expect(response).to render_template('shared/wikis/edit') @@ -432,7 +439,7 @@ RSpec.shared_examples 'wiki controller actions' do it 'renders 404' do expect do - subject + request end.not_to change { wiki.list_pages.size } expect(response).to have_gitlab_http_status(:not_found) diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb index c9910487798..2fff4137934 100644 --- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb @@ -11,6 +11,15 @@ RSpec.shared_examples 'an editable merge request' do expect(page).to have_content user.name end + find('.js-reviewer-search').click + page.within '.dropdown-menu-user' do + click_link user.name + end + expect(find('input[name="merge_request[reviewer_ids][]"]', visible: false).value).to match(user.id.to_s) + page.within '.js-reviewer-search' do + expect(page).to have_content user.name + end + click_button 'Milestone' page.within '.issue-milestone' do click_link milestone.title @@ -38,6 +47,10 @@ RSpec.shared_examples 'an editable merge request' do expect(page).to have_content user.name end + page.within '.reviewer' do + expect(page).to have_content user.name + end + page.within '.milestone' do expect(page).to have_content milestone.title end @@ -69,7 +82,7 @@ RSpec.shared_examples 'an editable merge request' do end it 'warns about version conflict' do - merge_request.update(title: "New title") + merge_request.update!(title: "New title") fill_in 'merge_request_title', with: 'bug 345' fill_in 'merge_request_description', with: 'bug description' @@ -124,16 +137,3 @@ end def get_textarea_height page.evaluate_script('document.getElementById("merge_request_description").offsetHeight') end - -RSpec.shared_examples 'an editable merge request with reviewers' do - it 'updates merge request', :js do - find('.js-reviewer-search').click - page.within '.dropdown-menu-user' do - click_link user.name - end - expect(find('input[name="merge_request[reviewer_ids][]"]', visible: false).value).to match(user.id.to_s) - page.within '.js-reviewer-search' do - expect(page).to have_content user.name - end - end -end 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 new file mode 100644 index 00000000000..ac1cc2da7e3 --- /dev/null +++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'issuable invite members experiments' do + context 'when invite_members_version_a experiment is enabled' do + before do + stub_experiment_for_user(invite_members_version_a: true) + end + + it 'shows a link for inviting members and follows through to the members page' do + project.add_maintainer(user) + visit issuable_path + + find('.block.assignee .edit-link').click + + wait_for_requests + + page.within '.dropdown-menu-user' do + expect(page).to have_link('Invite Members', href: project_project_members_path(project)) + expect(page).to have_selector('[data-track-event="click_invite_members"]') + expect(page).to have_selector('[data-track-label="edit_assignee"]') + end + + click_link 'Invite Members' + + expect(current_path).to eq project_project_members_path(project) + end + end + + context 'when invite_members_version_b experiment is enabled' do + before do + stub_experiment_for_user(invite_members_version_b: true) + end + + it 'shows a link for inviting members and follows through to modal' do + project.add_developer(user) + visit issuable_path + + find('.block.assignee .edit-link').click + + wait_for_requests + + page.within '.dropdown-menu-user' do + expect(page).to have_link('Invite Members', href: '#') + expect(page).to have_selector('[data-track-event="click_invite_members_version_b"]') + expect(page).to have_selector('[data-track-label="edit_assignee"]') + end + + click_link 'Invite Members' + + expect(page).to have_content("Oops, this feature isn't ready yet") + end + end + + context 'when no invite members experiments are enabled' do + it 'shows author in assignee dropdown and no invite link' do + project.add_maintainer(user) + visit issuable_path + + find('.block.assignee .edit-link').click + + wait_for_requests + + page.within '.dropdown-menu-user' do + expect(page).not_to have_link('Invite Members') + end + end + end +end diff --git a/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb index 19a5750cf6d..9d023d9514a 100644 --- a/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb +++ b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb @@ -38,7 +38,7 @@ RSpec.shared_examples 'multiple assignees merge request' do |action, save_button page.within '.issuable-sidebar' do page.within '.assignee' do # Closing dropdown to persist - click_link 'Edit' + click_link 'Apply' expect(page).to have_content user2.name end diff --git a/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb new file mode 100644 index 00000000000..48cde90bd9b --- /dev/null +++ b/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'multiple reviewers merge request' do |action, save_button_title| + it "#{action} a MR with multiple reviewers", :js do + find('.js-reviewer-search').click + page.within '.dropdown-menu-user' do + click_link user.name + click_link user2.name + end + + # Extra click needed in order to toggle the dropdown + find('.js-reviewer-search').click + + expect(all('input[name="merge_request[reviewer_ids][]"]', visible: false).map(&:value)) + .to match_array([user.id.to_s, user2.id.to_s]) + + page.within '.js-reviewer-search' do + expect(page).to have_content "#{user2.name} + 1 more" + end + + click_button save_button_title + + page.within '.issuable-sidebar' do + page.within '.reviewer' do + expect(page).to have_content '2 Reviewers' + + click_link 'Edit' + + expect(page).to have_content user.name + expect(page).to have_content user2.name + end + end + + page.within '.dropdown-menu-user' do + click_link user.name + end + + page.within '.issuable-sidebar' do + page.within '.reviewer' do + # Closing dropdown to persist + click_link 'Edit' + + expect(page).to have_content user2.name + end + end + end +end diff --git a/spec/support/shared_examples/features/navbar_shared_examples.rb b/spec/support/shared_examples/features/navbar_shared_examples.rb index 91a4048fa7c..c768e95c45a 100644 --- a/spec/support/shared_examples/features/navbar_shared_examples.rb +++ b/spec/support/shared_examples/features/navbar_shared_examples.rb @@ -3,12 +3,14 @@ RSpec.shared_examples 'verified navigation bar' do let(:expected_structure) do structure.compact! - structure.each { |s| s[:nav_sub_items].compact! } + structure.each { |s| s[:nav_sub_items]&.compact! } structure end it 'renders correctly' do current_structure = page.all('.sidebar-top-level-items > li', class: ['!hidden']).map do |item| + next if item.find_all('a').empty? + nav_item = item.find_all('a').first.text.gsub(/\s+\d+$/, '') # remove counts at the end nav_sub_items = item.all('.sidebar-sub-level-items > li', class: ['!fly-out-top-item']).map do |list_item| @@ -16,7 +18,7 @@ RSpec.shared_examples 'verified navigation bar' do end { nav_item: nav_item, nav_sub_items: nav_sub_items } - end + end.compact expect(current_structure).to eq(expected_structure) end diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb index f201421e827..4d2e13aa5bc 100644 --- a/spec/support/shared_examples/features/packages_shared_examples.rb +++ b/spec/support/shared_examples/features/packages_shared_examples.rb @@ -84,11 +84,11 @@ RSpec.shared_examples 'shared package sorting' do let(:packages) { [package_two, package_one] } end - it_behaves_like 'correctly sorted packages list', 'Created' do + it_behaves_like 'correctly sorted packages list', 'Published' do let(:packages) { [package_two, package_one] } end - it_behaves_like 'correctly sorted packages list', 'Created', ascending: true do + it_behaves_like 'correctly sorted packages list', 'Published', ascending: true do let(:packages) { [package_one, package_two] } end end diff --git a/spec/support/shared_examples/features/page_description_shared_examples.rb b/spec/support/shared_examples/features/page_description_shared_examples.rb new file mode 100644 index 00000000000..81653220b4c --- /dev/null +++ b/spec/support/shared_examples/features/page_description_shared_examples.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'page meta description' do |expected_description| + it 'renders the page with description, og:description, and twitter:description meta tags that contains a plain-text version of the markdown', :aggregate_failures do + %w(name='description' property='og:description' property='twitter:description').each do |selector| + expect(page).to have_selector("meta[#{selector}][content='#{expected_description}']", visible: false) + end + end +end diff --git a/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb b/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb new file mode 100644 index 00000000000..a2d2143271c --- /dev/null +++ b/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'when the deploy_keys_on_protected_branches FF is turned on' do + before do + stub_feature_flags(deploy_keys_on_protected_branches: true) + project.add_maintainer(user) + sign_in(user) + end + + let(:dropdown_sections_minus_deploy_keys) { all_dropdown_sections - ['Deploy Keys'] } + + context 'when deploy keys are enabled to this project' do + let!(:deploy_key_1) { create(:deploy_key, title: 'title 1', projects: [project]) } + let!(:deploy_key_2) { create(:deploy_key, title: 'title 2', projects: [project]) } + + context 'when only one deploy key can push' do + before do + deploy_key_1.deploy_keys_projects.first.update!(can_push: true) + end + + it "shows all dropdown sections in the 'Allowed to push' main dropdown, with only one deploy key" do + visit project_protected_branches_path(project) + + find(".js-allowed-to-push").click + wait_for_requests + + within('.qa-allowed-to-push-dropdown') do + dropdown_headers = page.all('.dropdown-header').map(&:text) + + expect(dropdown_headers).to contain_exactly(*all_dropdown_sections) + expect(page).to have_content('title 1') + expect(page).not_to have_content('title 2') + end + end + + it "shows all sections but not deploy keys in the 'Allowed to merge' main dropdown" do + visit project_protected_branches_path(project) + + find(".js-allowed-to-merge").click + wait_for_requests + + within('.qa-allowed-to-merge-dropdown') do + dropdown_headers = page.all('.dropdown-header').map(&:text) + + expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys) + end + end + + it "shows all sections in the 'Allowed to push' update dropdown" do + create(:protected_branch, :no_one_can_push, project: project, name: 'master') + + visit project_protected_branches_path(project) + + within(".js-protected-branch-edit-form") do + find(".js-allowed-to-push").click + wait_for_requests + + dropdown_headers = page.all('.dropdown-header').map(&:text) + + expect(dropdown_headers).to contain_exactly(*all_dropdown_sections) + end + end + end + + context 'when no deploy key can push' do + it "just shows all sections but not deploy keys in the 'Allowed to push' dropdown" do + visit project_protected_branches_path(project) + + find(".js-allowed-to-push").click + wait_for_requests + + within('.qa-allowed-to-push-dropdown') do + dropdown_headers = page.all('.dropdown-header').map(&:text) + + expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys) + end + end + + it "just shows all sections but not deploy keys in the 'Allowed to push' update dropdown" do + create(:protected_branch, :no_one_can_push, project: project, name: 'master') + + visit project_protected_branches_path(project) + + within(".js-protected-branch-edit-form") do + find(".js-allowed-to-push").click + wait_for_requests + + dropdown_headers = page.all('.dropdown-header').map(&:text) + + expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys) + end + end + end + end +end diff --git a/spec/support/shared_examples/features/snippets_shared_examples.rb b/spec/support/shared_examples/features/snippets_shared_examples.rb index 8d68b1e4c0a..bd1a67f3bb5 100644 --- a/spec/support/shared_examples/features/snippets_shared_examples.rb +++ b/spec/support/shared_examples/features/snippets_shared_examples.rb @@ -84,7 +84,7 @@ RSpec.shared_examples 'show and render proper snippet blob' do expect(page).not_to have_selector('.js-blob-viewer-switcher') # shows an enabled copy button - expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + expect(page).to have_button('Copy file contents', disabled: false) # shows a raw button expect(page).to have_link('Open raw') @@ -106,7 +106,6 @@ RSpec.shared_examples 'show and render proper snippet blob' do it 'displays the blob using the rich viewer' do aggregate_failures do # hides the simple viewer - expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false) expect(page).to have_selector('.blob-viewer[data-type="rich"]') # shows rendered Markdown @@ -116,7 +115,7 @@ RSpec.shared_examples 'show and render proper snippet blob' do expect(page).to have_selector('.js-blob-viewer-switcher') # shows a disabled copy button - expect(page).to have_selector('.js-copy-blob-source-btn.disabled') + expect(page).to have_button('Copy file contents', disabled: true) # shows a raw button expect(page).to have_link('Open raw') @@ -128,7 +127,7 @@ RSpec.shared_examples 'show and render proper snippet blob' do context 'switching to the simple viewer' do before do - find('.js-blob-viewer-switch-btn[data-viewer=simple]').click + find_button('Display source').click wait_for_requests end @@ -137,19 +136,18 @@ RSpec.shared_examples 'show and render proper snippet blob' do aggregate_failures do # hides the rich viewer expect(page).to have_selector('.blob-viewer[data-type="simple"]') - expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false) # shows highlighted Markdown code expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)") # shows an enabled copy button - expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + expect(page).to have_button('Copy file contents', disabled: false) end end context 'switching to the rich viewer again' do before do - find('.js-blob-viewer-switch-btn[data-viewer=rich]').click + find_button('Display rendered file').click wait_for_requests end @@ -157,11 +155,11 @@ RSpec.shared_examples 'show and render proper snippet blob' do it 'displays the blob using the rich viewer' do aggregate_failures do # hides the simple viewer - expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false) expect(page).to have_selector('.blob-viewer[data-type="rich"]') - # shows an enabled copy button - expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + # Used to show an enabled copy button since the code has already been fetched + # Will be resolved in https://gitlab.com/gitlab-org/gitlab/-/issues/262389 + expect(page).to have_button('Copy file contents', disabled: true) end end end @@ -169,7 +167,8 @@ RSpec.shared_examples 'show and render proper snippet blob' do end context 'visiting with a line number anchor' do - let(:anchor) { 'L1' } + # L1 used to work and will be revisited in https://gitlab.com/gitlab-org/gitlab/-/issues/262391 + let(:anchor) { 'LC1' } it 'displays the blob using the simple viewer' do subject @@ -177,7 +176,6 @@ RSpec.shared_examples 'show and render proper snippet blob' do aggregate_failures do # hides the rich viewer expect(page).to have_selector('.blob-viewer[data-type="simple"]') - expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false) # highlights the line in question expect(page).to have_selector('#LC1.hll') @@ -186,7 +184,7 @@ RSpec.shared_examples 'show and render proper snippet blob' do expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)") # shows an enabled copy button - expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + expect(page).to have_button('Copy file contents', disabled: false) end end end diff --git a/spec/support/shared_examples/features/wiki_file_attachments_shared_examples.rb b/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb index d30e8241da0..0ef1ccdfe57 100644 --- a/spec/support/shared_examples/features/wiki_file_attachments_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb @@ -1,14 +1,12 @@ # frozen_string_literal: true # Requires a context containing: -# project +# wiki RSpec.shared_examples 'wiki file attachments' do include DropzoneHelper context 'uploading attachments', :js do - let(:wiki) { project.wiki } - def attach_with_dropzone(wait = false) dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, wait) end diff --git a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb new file mode 100644 index 00000000000..44d82d2e753 --- /dev/null +++ b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb @@ -0,0 +1,245 @@ +# frozen_string_literal: true + +# Requires a context containing: +# wiki +# user + +RSpec.shared_examples 'User creates wiki page' do + include WikiHelpers + + before do + sign_in(user) + end + + context "when wiki is empty" do + before do |example| + visit wiki_path(wiki) + + wait_for_svg_to_be_loaded(example) + + click_link "Create your first page" + end + + it "shows validation error message" do + page.within(".wiki-form") do + fill_in(:wiki_content, with: "") + + click_on("Create page") + end + + expect(page).to have_content("The form contains the following error:").and have_content("Content can't be blank") + + page.within(".wiki-form") do + fill_in(:wiki_content, with: "[link test](test)") + + click_on("Create page") + end + + expect(page).to have_content("Home").and have_content("link test") + + click_link("link test") + + expect(page).to have_content("Create New Page") + end + + it "shows non-escaped link in the pages list" do + fill_in(:wiki_title, with: "one/two/three-test") + + page.within(".wiki-form") do + fill_in(:wiki_content, with: "wiki content") + + click_on("Create page") + end + + expect(current_path).to include("one/two/three-test") + expect(page).to have_link(href: wiki_page_path(wiki, 'one/two/three-test')) + end + + it "has `Create home` as a commit message", :js do + wait_for_requests + + expect(page).to have_field("wiki[message]", with: "Create home") + end + + it "creates a page from the home page" do + fill_in(:wiki_content, with: "[test](test)\n[GitLab API doc](api)\n[Rake tasks](raketasks)\n# Wiki header\n") + fill_in(:wiki_message, with: "Adding links to wiki") + + page.within(".wiki-form") do + click_button("Create page") + end + + expect(current_path).to eq(wiki_page_path(wiki, "home")) + expect(page).to have_content("test GitLab API doc Rake tasks Wiki header") + .and have_content("Home") + .and have_content("Last edited by #{user.name}") + .and have_header_with_correct_id_and_link(1, "Wiki header", "wiki-header") + + click_link("test") + + expect(current_path).to eq(wiki_page_path(wiki, "test")) + + page.within(:css, ".nav-text") do + expect(page).to have_content("Create New Page") + end + + click_link("Home") + + expect(current_path).to eq(wiki_page_path(wiki, "home")) + + click_link("GitLab API") + + expect(current_path).to eq(wiki_page_path(wiki, "api")) + + page.within(:css, ".nav-text") do + expect(page).to have_content("Create") + end + + click_link("Home") + + expect(current_path).to eq(wiki_page_path(wiki, "home")) + + click_link("Rake tasks") + + expect(current_path).to eq(wiki_page_path(wiki, "raketasks")) + + page.within(:css, ".nav-text") do + expect(page).to have_content("Create") + end + end + + it "creates ASCII wiki with LaTeX blocks", :js do + stub_application_setting(plantuml_url: "http://localhost", plantuml_enabled: true) + + ascii_content = <<~MD + :stem: latexmath + + [stem] + ++++ + \\sqrt{4} = 2 + ++++ + + another part + + [latexmath] + ++++ + \\beta_x \\gamma + ++++ + + stem:[2+2] is 4 + MD + + find("#wiki_format option[value=asciidoc]").select_option + + fill_in(:wiki_content, with: ascii_content) + + page.within(".wiki-form") do + click_button("Create page") + end + + page.within ".md" do + expect(page).to have_selector(".katex", count: 3).and have_content("2+2 is 4") + end + end + + it 'creates a wiki page with Org markup', :aggregate_failures do + org_content = <<~ORG + * Heading + ** Subheading + [[home][Link to Home]] + ORG + + page.within('.wiki-form') do + find('#wiki_format option[value=org]').select_option + fill_in(:wiki_content, with: org_content) + click_button('Create page') + end + + expect(page).to have_selector('h1', text: 'Heading') + expect(page).to have_selector('h2', text: 'Subheading') + expect(page).to have_link(href: wiki_page_path(wiki, 'home')) + end + + it_behaves_like 'wiki file attachments' + end + + context "when wiki is not empty", :js do + before do + create(:wiki_page, wiki: wiki, title: 'home', content: 'Home page') + + visit wiki_path(wiki) + end + + context "via the `new wiki page` page" do + it "creates a page with a single word" do + click_link("New page") + + page.within(".wiki-form") do + fill_in(:wiki_title, with: "foo") + fill_in(:wiki_content, with: "My awesome wiki!") + end + + # Commit message field should have correct value. + expect(page).to have_field("wiki[message]", with: "Create foo") + + click_button("Create page") + + expect(page).to have_content("foo") + .and have_content("Last edited by #{user.name}") + .and have_content("My awesome wiki!") + end + + it "creates a page with spaces in the name" do + click_link("New page") + + page.within(".wiki-form") do + fill_in(:wiki_title, with: "Spaces in the name") + fill_in(:wiki_content, with: "My awesome wiki!") + end + + # Commit message field should have correct value. + expect(page).to have_field("wiki[message]", with: "Create Spaces in the name") + + click_button("Create page") + + expect(page).to have_content("Spaces in the name") + .and have_content("Last edited by #{user.name}") + .and have_content("My awesome wiki!") + end + + it "creates a page with hyphens in the name" do + click_link("New page") + + page.within(".wiki-form") do + fill_in(:wiki_title, with: "hyphens-in-the-name") + fill_in(:wiki_content, with: "My awesome wiki!") + end + + # Commit message field should have correct value. + expect(page).to have_field("wiki[message]", with: "Create hyphens in the name") + + page.within(".wiki-form") do + fill_in(:wiki_content, with: "My awesome wiki!") + + click_button("Create page") + end + + expect(page).to have_content("hyphens in the name") + .and have_content("Last edited by #{user.name}") + .and have_content("My awesome wiki!") + end + end + + it "shows the emoji autocompletion dropdown" do + click_link("New page") + + page.within(".wiki-form") do + find("#wiki_content").native.send_keys("") + + fill_in(:wiki_content, with: ":") + end + + expect(page).to have_selector(".atwho-view") + end + end +end diff --git a/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb new file mode 100644 index 00000000000..e1fd9c8dbec --- /dev/null +++ b/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# Requires a context containing: +# wiki +# user + +RSpec.shared_examples 'User deletes wiki page' do + include WikiHelpers + + let(:wiki_page) { create(:wiki_page, wiki: wiki) } + + before do + sign_in(user) + visit wiki_page_path(wiki, wiki_page) + end + + it 'deletes a page', :js do + click_on('Edit') + click_on('Delete') + find('.modal-footer .btn-danger').click + + expect(page).to have_content('Page was successfully deleted') + end +end diff --git a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb new file mode 100644 index 00000000000..a22d98f20c4 --- /dev/null +++ b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +# Requires a context containing: +# wiki +# user + +RSpec.shared_examples 'User previews wiki changes' do + let(:wiki_page) { build(:wiki_page, wiki: wiki) } + + before do + sign_in(user) + end + + shared_examples 'relative links' do + let_it_be(:page_content) do + <<~HEREDOC + Some text so key event for [ does not trigger an incorrect replacement. + [regular link](regular) + [relative link 1](../relative) + [relative link 2](./relative) + [relative link 3](./e/f/relative) + [spaced link](title with spaces) + HEREDOC + end + + def relative_path(path) + (Pathname.new(wiki.wiki_base_path) + File.dirname(wiki_page.path).tr(' ', '-') + path).to_s + end + + shared_examples "rewrites relative links" do + specify do + expect(element).to have_link('regular link', href: wiki.wiki_base_path + '/regular') + expect(element).to have_link('spaced link', href: wiki.wiki_base_path + '/title%20with%20spaces') + + expect(element).to have_link('relative link 1', href: relative_path('../relative')) + expect(element).to have_link('relative link 2', href: relative_path('./relative')) + expect(element).to have_link('relative link 3', href: relative_path('./e/f/relative')) + end + end + + context "when there are no spaces or hyphens in the page name" do + let(:wiki_page) { build(:wiki_page, wiki: wiki, title: 'a/b/c/d', content: page_content) } + + it_behaves_like 'rewrites relative links' + end + + context "when there are spaces in the page name" do + let(:wiki_page) { build(:wiki_page, wiki: wiki, title: 'a page/b page/c page/d page', content: page_content) } + + it_behaves_like 'rewrites relative links' + end + + context "when there are hyphens in the page name" do + let(:wiki_page) { build(:wiki_page, wiki: wiki, title: 'a-page/b-page/c-page/d-page', content: page_content) } + + it_behaves_like 'rewrites relative links' + end + end + + context "when rendering a new wiki page", :js do + before do + wiki_page.create # rubocop:disable Rails/SaveBang + visit wiki_page_path(wiki, wiki_page) + end + + it_behaves_like 'relative links' do + let(:element) { page.find('[data-testid="wiki_page_content"]') } + end + end + + context "when previewing an existing wiki page", :js do + let(:preview) { page.find('.md-preview-holder') } + + before do + wiki_page.create # rubocop:disable Rails/SaveBang + visit wiki_page_path(wiki, wiki_page, action: :edit) + end + + it_behaves_like 'relative links' do + before do + click_on 'Preview' + end + + let(:element) { preview } + end + + it 'renders content with CommonMark' do + fill_in :wiki_content, with: "1. one\n - sublist\n" + click_on "Preview" + + # the above generates two separate lists (not embedded) in CommonMark + expect(preview).to have_content("sublist") + expect(preview).not_to have_xpath("//ol//li//ul") + end + + it "does not linkify double brackets inside code blocks as expected" do + fill_in :wiki_content, with: <<-HEREDOC + `[[do_not_linkify]]` + ``` + [[also_do_not_linkify]] + ``` + HEREDOC + click_on "Preview" + + expect(preview).to have_content("do_not_linkify") + expect(preview).to have_content('[[do_not_linkify]]') + expect(preview).to have_content('[[also_do_not_linkify]]') + end + 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 new file mode 100644 index 00000000000..1a5f8d7d8df --- /dev/null +++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb @@ -0,0 +1,231 @@ +# frozen_string_literal: true + +# Requires a context containing: +# wiki +# user + +RSpec.shared_examples 'User updates wiki page' do + include WikiHelpers + + before do + sign_in(user) + end + + context 'when wiki is empty' do + before do |example| + visit(wiki_path(wiki)) + + wait_for_svg_to_be_loaded(example) + + click_link "Create your first page" + end + + it 'redirects back to the home edit page' do + page.within(:css, '.wiki-form .form-actions') do + click_on('Cancel') + end + + expect(current_path).to eq wiki_path(wiki) + end + + it 'updates a page that has a path', :js do + fill_in(:wiki_title, with: 'one/two/three-test') + + page.within '.wiki-form' do + fill_in(:wiki_content, with: 'wiki content') + click_on('Create page') + end + + expect(current_path).to include('one/two/three-test') + expect(find('.wiki-pages')).to have_content('three') + + first(:link, text: 'three').click + + expect(find('[data-testid="wiki_page_title"]')).to have_content('three') + + click_on('Edit') + + expect(current_path).to include('one/two/three-test') + expect(page).to have_content('Edit Page') + + fill_in('Content', with: 'Updated Wiki Content') + click_on('Save changes') + + expect(page).to have_content('Updated Wiki Content') + end + + it_behaves_like 'wiki file attachments' + end + + context 'when wiki is not empty' do + let!(:wiki_page) { create(:wiki_page, wiki: wiki, title: 'home', content: 'Home page') } + + before do + visit(wiki_path(wiki)) + + click_link('Edit') + end + + it 'updates a page', :js do + # Commit message field should have correct value. + expect(page).to have_field('wiki[message]', with: 'Update home') + + fill_in(:wiki_content, with: 'My awesome wiki!') + click_button('Save changes') + + expect(page).to have_content('Home') + expect(page).to have_content("Last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') + end + + it 'updates the commit message as the title is changed', :js do + fill_in(:wiki_title, with: '& < > \ \ { } &') + + expect(page).to have_field('wiki[message]', with: 'Update & < > \ \ { } &') + end + + it 'correctly escapes the commit message entities', :js do + fill_in(:wiki_title, with: 'Wiki title') + + expect(page).to have_field('wiki[message]', with: 'Update Wiki title') + end + + it 'shows a validation error message' do + fill_in(:wiki_content, with: '') + click_button('Save changes') + + expect(page).to have_selector('.wiki-form') + expect(page).to have_content('Edit Page') + expect(page).to have_content('The form contains the following error:') + expect(page).to have_content("Content can't be blank") + expect(find('textarea#wiki_content').value).to eq('') + end + + it 'shows the emoji autocompletion dropdown', :js do + find('#wiki_content').native.send_keys('') + fill_in(:wiki_content, with: ':') + + expect(page).to have_selector('.atwho-view') + end + + it 'shows the error message' do + wiki_page.update(content: 'Update') # rubocop:disable Rails/SaveBang + + click_button('Save changes') + + expect(page).to have_content('Someone edited the page the same time you did.') + end + + it 'updates a page' do + fill_in('Content', with: 'Updated Wiki Content') + click_on('Save changes') + + expect(page).to have_content('Updated Wiki Content') + end + + it 'cancels editing of a page' do + page.within(:css, '.wiki-form .form-actions') do + click_on('Cancel') + end + + expect(current_path).to eq(wiki_page_path(wiki, wiki_page)) + end + + it_behaves_like 'wiki file attachments' + end + + context 'when the page is in a subdir' do + let(:page_name) { 'page_name' } + let(:page_dir) { "foo/bar/#{page_name}" } + let!(:wiki_page) { create(:wiki_page, wiki: wiki, title: page_dir, content: 'Home page') } + + before do + visit wiki_page_path(wiki, wiki_page, action: :edit) + end + + it 'moves the page to the root folder' do + fill_in(:wiki_title, with: "/#{page_name}") + + click_button('Save changes') + + expect(current_path).to eq(wiki_page_path(wiki, page_name)) + end + + it 'moves the page to other dir' do + new_page_dir = "foo1/bar1/#{page_name}" + + fill_in(:wiki_title, with: new_page_dir) + + click_button('Save changes') + + expect(current_path).to eq(wiki_page_path(wiki, new_page_dir)) + end + + it 'remains in the same place if title has not changed' do + original_path = wiki_page_path(wiki, wiki_page) + + fill_in(:wiki_title, with: page_name) + + click_button('Save changes') + + expect(current_path).to eq(original_path) + end + + it 'can be moved to a different dir with a different name' do + new_page_dir = "foo1/bar1/new_page_name" + + fill_in(:wiki_title, with: new_page_dir) + + click_button('Save changes') + + expect(current_path).to eq(wiki_page_path(wiki, new_page_dir)) + end + + it 'can be renamed and moved to the root folder' do + new_name = 'new_page_name' + + fill_in(:wiki_title, with: "/#{new_name}") + + click_button('Save changes') + + expect(current_path).to eq(wiki_page_path(wiki, new_name)) + end + + it 'squishes the title before creating the page' do + new_page_dir = " foo1 / bar1 / #{page_name} " + + fill_in(:wiki_title, with: new_page_dir) + + click_button('Save changes') + + expect(current_path).to eq(wiki_page_path(wiki, "foo1/bar1/#{page_name}")) + end + + it_behaves_like 'wiki file attachments' + end + + context 'when an existing page exceeds the content size limit' do + let!(:wiki_page) { create(:wiki_page, wiki: wiki, content: "one\ntwo\nthree") } + + before do + stub_application_setting(wiki_page_max_content_bytes: 10) + + visit wiki_page_path(wiki_page.wiki, wiki_page, action: :edit) + end + + it 'allows changing the title if the content does not change' do + fill_in 'Title', with: 'new title' + click_on 'Save changes' + + expect(page).to have_content('Wiki was successfully updated.') + end + + it 'shows a validation error when trying to change the content' do + fill_in 'Content', with: 'new content' + click_on 'Save changes' + + expect(page).to have_content('The form contains the following error:') + expect(page).to have_content('Content is too long (11 Bytes). The maximum size is 10 Bytes.') + end + end +end diff --git a/spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb new file mode 100644 index 00000000000..0330b345a18 --- /dev/null +++ b/spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Requires a context containing: +# wiki +# user + +RSpec.shared_examples 'User uses wiki shortcuts' do + let(:wiki_page) { create(:wiki_page, wiki: wiki, title: 'home', content: 'Home page') } + + before do + sign_in(user) + visit wiki_page_path(wiki, wiki_page) + end + + it 'Visit edit wiki page using "e" keyboard shortcut', :js do + find('body').native.send_key('e') + + expect(find('.wiki-page-title')).to have_content('Edit Page') + end +end diff --git a/spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb new file mode 100644 index 00000000000..3b2fda4e05b --- /dev/null +++ b/spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'User views AsciiDoc page with includes' do + let_it_be(:wiki_content_selector) { '[data-qa-selector=wiki_page_content]' } + let!(:included_wiki_page) { create_wiki_page('included_page', content: 'Content from the included page')} + let!(:wiki_page) { create_wiki_page('home', content: "Content from the main page.\ninclude::included_page.asciidoc[]") } + + def create_wiki_page(title, content:) + attrs = { + title: title, + content: content, + format: :asciidoc + } + + create(:wiki_page, wiki: wiki, **attrs) + end + + before do + sign_in(user) + end + + context 'when the file being included exists', :js do + it 'includes the file contents' do + visit(wiki_page_path(wiki, wiki_page)) + + page.within(:css, wiki_content_selector) do + expect(page).to have_content('Content from the main page. Content from the included page') + end + end + + context 'when there are multiple versions of the wiki pages' do + before do + # rubocop:disable Rails/SaveBang + included_wiki_page.update(message: 'updated included file', content: 'Updated content from the included page') + wiki_page.update(message: 'updated wiki page', content: "Updated content from the main page.\ninclude::included_page.asciidoc[]") + # rubocop:enable Rails/SaveBang + end + + let(:latest_version_id) { wiki_page.versions.first.id } + let(:oldest_version_id) { wiki_page.versions.last.id } + + context 'viewing the latest version' do + it 'includes the latest content' do + visit(wiki_page_path(wiki, wiki_page, version_id: latest_version_id)) + + page.within(:css, wiki_content_selector) do + expect(page).to have_content('Updated content from the main page. Updated content from the included page') + end + end + end + + context 'viewing the original version' do + it 'includes the content from the original version' do + visit(wiki_page_path(wiki, wiki_page, version_id: oldest_version_id)) + + page.within(:css, wiki_content_selector) do + expect(page).to have_content('Content from the main page. Content from the included page') + end + end + end + end + end + + context 'when the file being included does not exist', :js do + before do + included_wiki_page.delete + end + + it 'outputs an error' do + visit(wiki_page_path(wiki, wiki_page)) + + page.within(:css, wiki_content_selector) do + expect(page).to have_content('Content from the main page. [ERROR: include::included_page.asciidoc[] - unresolved directive]') + end + end + end +end diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb new file mode 100644 index 00000000000..d7f5b485a82 --- /dev/null +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +# Requires a context containing: +# wiki + +RSpec.shared_examples 'User views empty wiki' do + let(:element) { page.find('.row.empty-state') } + let(:container_name) { wiki.container.class.name.humanize(capitalize: false) } + let(:confluence_link) { 'Enable the Confluence Wiki integration' } + + shared_examples 'wiki is not found' do + it 'shows an error message' do + visit wiki_path(wiki) + + if @current_user + expect(page).to have_content('Page Not Found') + else + expect(page).to have_content('You need to sign in') + end + end + end + + shared_examples 'empty wiki message' do |writable: false, issuable: false, confluence: false| + # This mirrors the logic in: + # - app/views/shared/empty_states/_wikis.html.haml + # - WikiHelper#wiki_empty_state_messages + it 'shows the empty state message with the expected elements' do + visit wiki_path(wiki) + + if writable + expect(element).to have_content("The wiki lets you write documentation for your #{container_name}") + else + expect(element).to have_content("This #{container_name} has no wiki pages") + expect(element).to have_content("You must be a #{container_name} member") + end + + if issuable && !writable + expect(element).to have_content("improve the wiki for this #{container_name}") + expect(element).to have_link("issue tracker", href: project_issues_path(project)) + expect(element).to have_link("Suggest wiki improvement", href: new_project_issue_path(project)) + else + expect(element).not_to have_content("improve the wiki for this #{container_name}") + expect(element).not_to have_link("issue tracker") + expect(element).not_to have_link("Suggest wiki improvement") + end + + if confluence + expect(element).to have_link(confluence_link) + else + expect(element).not_to have_link(confluence_link) + end + + if writable + element.click_link 'Create your first page' + + expect(page).to have_button('Create page') + else + expect(element).not_to have_link('Create your first page') + end + end + end +end diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb new file mode 100644 index 00000000000..85eedbf4cc5 --- /dev/null +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb @@ -0,0 +1,274 @@ +# frozen_string_literal: true + +# Requires a context containing: +# wiki +# user + +RSpec.shared_examples 'User views a wiki page' do + include WikiHelpers + + let(:path) { 'image.png' } + let(:wiki_page) do + create(:wiki_page, + wiki: wiki, + title: 'home', content: "Look at this [image](#{path})\n\n ![alt text](#{path})") + end + + before do + sign_in(user) + end + + context 'when wiki is empty', :js do + before do + visit wiki_path(wiki) + + wait_for_svg_to_be_loaded + + click_link "Create your first page" + + fill_in(:wiki_title, with: 'one/two/three-test') + + page.within('.wiki-form') do + fill_in(:wiki_content, with: 'wiki content') + click_on('Create page') + end + + expect(page).to have_content('Wiki was successfully updated.') + end + + it 'shows the history of a page that has a path' do + expect(current_path).to include('one/two/three-test') + + first(:link, text: 'three').click + click_on('Page history') + + expect(current_path).to include('one/two/three-test') + + page.within(:css, '.nav-text') do + expect(page).to have_content('History') + end + end + + it 'shows an old version of a page' do + expect(current_path).to include('one/two/three-test') + expect(find('.wiki-pages')).to have_content('three') + + first(:link, text: 'three').click + + expect(find('[data-testid="wiki_page_title"]')).to have_content('three') + + click_on('Edit') + + expect(current_path).to include('one/two/three-test') + expect(page).to have_content('Edit Page') + + fill_in('Content', with: 'Updated Wiki Content') + click_on('Save changes') + + expect(page).to have_content('Wiki was successfully updated.') + + click_on('Page history') + + within('.nav-text') do + expect(page).to have_content('History') + end + + within('.wiki-history') do + expect(page).to have_css('a[href*="?version_id"]', count: 4) + end + end + end + + context 'when a page does not have history' do + before do + visit(wiki_page_path(wiki, wiki_page)) + end + + it 'shows all the pages' do + expect(page).to have_content(user.name) + expect(find('.wiki-pages')).to have_content(wiki_page.title.capitalize) + end + + context 'shows a file stored in a page' do + let(:path) { upload_file_to_wiki(wiki, user, 'dk.png') } + + it do + expect(page).to have_xpath("//img[@data-src='#{wiki.wiki_base_path}/#{path}']") + expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/#{path}") + + click_on('image') + + expect(current_path).to match("wikis/#{path}") + expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved + end + end + + it 'shows the creation page if file does not exist' do + expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/#{path}") + + click_on('image') + + expect(current_path).to match("wikis/#{path}") + expect(page).to have_content('Create New Page') + end + end + + context 'when a page has history' do + before do + wiki_page.update(message: 'updated home', content: 'updated [some link](other-page)') # rubocop:disable Rails/SaveBang + end + + it 'shows the page history' do + visit(wiki_page_path(wiki, wiki_page)) + + expect(page).to have_selector('[data-testid="wiki_edit_button"]') + + click_on('Page history') + + expect(page).to have_content(user.name) + expect(page).to have_content("#{user.username} created page: home") + expect(page).to have_content('updated home') + end + + it 'does not show the "Edit" button' do + visit(wiki_page_path(wiki, wiki_page, version_id: wiki_page.versions.last.id)) + + expect(page).not_to have_selector('[data-testid="wiki_edit_button"]') + end + + context 'show the diff' do + def expect_diff_links(commit) + diff_path = wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff) + + expect(page).to have_link('Hide whitespace changes', href: "#{diff_path}&w=1") + expect(page).to have_link('Inline', href: "#{diff_path}&view=inline") + expect(page).to have_link('Side-by-side', href: "#{diff_path}&view=parallel") + expect(page).to have_link("View page @ #{commit.short_id}", href: wiki_page_path(wiki, wiki_page, version_id: commit)) + expect(page).to have_css('.diff-file[data-blob-diff-path="%s"]' % diff_path) + end + + it 'links to the correct diffs' do + visit wiki_page_path(wiki, wiki_page, action: :history) + + commit1 = wiki.commit('HEAD^') + commit2 = wiki.commit + + expect(page).to have_link('created page: home', href: wiki_page_path(wiki, wiki_page, version_id: commit1, action: :diff)) + 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 + commit = wiki.commit + visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff) + + expect(page).to have_content('by John Doe') + expect(page).to have_content('updated home') + expect(page).to have_content('Showing 1 changed file with 1 addition and 3 deletions') + expect(page).to have_content('some link') + + expect_diff_links(commit) + end + + it 'between two old versions of a page' 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) + + expect(page).to have_content('by John Doe') + expect(page).to have_content('updated home') + expect(page).to have_content('Showing 1 changed file with 1 addition and 3 deletions') + expect(page).to have_content('some link') + expect(page).not_to have_content('latest home change') + expect(page).not_to have_content('another link') + + expect_diff_links(commit) + end + + it 'for the oldest version of a page' do + commit = wiki.commit('HEAD^') + visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff) + + expect(page).to have_content('by John Doe') + expect(page).to have_content('created page: home') + expect(page).to have_content('Showing 1 changed file with 4 additions and 0 deletions') + expect(page).to have_content('Look at this') + + expect_diff_links(commit) + end + end + end + + context 'when a page has special characters in its title' do + let(:title) { '<foo> !@#$%^&*()[]{}=_+\'"\\|<>? <bar>' } + + before do + wiki_page.update(title: title ) # rubocop:disable Rails/SaveBang + end + + it 'preserves the special characters' do + visit(wiki_page_path(wiki, wiki_page)) + + expect(page).to have_css('[data-testid="wiki_page_title"]', text: title) + expect(page).to have_css('.wiki-pages li', text: title) + end + end + + context 'when a page has XSS in its title or content' do + let(:title) { '<script>alert("title")<script>' } + + before do + wiki_page.update(title: title, content: 'foo <script>alert("content")</script> bar') # rubocop:disable Rails/SaveBang + end + + it 'safely displays the page' do + visit(wiki_page_path(wiki, wiki_page)) + + expect(page).to have_selector('[data-testid="wiki_page_title"]', text: title) + expect(page).to have_content('foo bar') + end + end + + context 'when a page has XSS in its message' do + before do + wiki_page.update(message: '<script>alert(true)<script>', content: 'XSS update') # rubocop:disable Rails/SaveBang + end + + it 'safely displays the message' do + visit(wiki_page_path(wiki, wiki_page, action: :history)) + + expect(page).to have_content('<script>alert(true)<script>') + end + end + + context 'when page has invalid content encoding' do + let(:content) { (+'whatever').force_encoding('ISO-8859-1') } + + before do + allow(Gitlab::EncodingHelper).to receive(:encode!).and_return(content) + + visit(wiki_page_path(wiki, wiki_page)) + end + + it 'does not show "Edit" button' do + expect(page).not_to have_selector('[data-testid="wiki_edit_button"]') + end + + it 'shows error' do + page.within(:css, '.flash-notice') do + expect(page).to have_content('The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.') + end + end + end + + it 'opens a default wiki page', :js do + visit wiki.container.web_url + + find('.shortcuts-wiki').click + + wait_for_svg_to_be_loaded + + click_link "Create your first page" + + expect(page).to have_content('Create New Page') + end +end diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_pages_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_pages_shared_examples.rb new file mode 100644 index 00000000000..314c2074eee --- /dev/null +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_pages_shared_examples.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +# Requires a context containing: +# wiki +# user + +RSpec.shared_examples 'User views wiki pages' do + include WikiHelpers + + let!(:wiki_page1) do + create(:wiki_page, wiki: wiki, title: '3 home', content: '3') + end + + let!(:wiki_page2) do + create(:wiki_page, wiki: wiki, title: '1 home', content: '1') + end + + let!(:wiki_page3) do + create(:wiki_page, wiki: wiki, title: '2 home', content: '2') + end + + let(:pages) do + page.find('.wiki-pages-list').all('li').map { |li| li.find('a') } + end + + before do + sign_in(user) + visit(wiki_path(wiki, action: :pages)) + end + + context 'ordered by title' do + let(:pages_ordered_by_title) { [wiki_page2, wiki_page3, wiki_page1] } + + context 'asc' do + it 'pages are displayed in direct order' do + pages.each.with_index do |page_title, index| + expect(page_title.text).to eq(pages_ordered_by_title[index].title) + end + end + end + + context 'desc' do + before do + page.within('.wiki-sort-dropdown') do + page.find('.rspec-reverse-sort').click + end + end + + it 'pages are displayed in reversed order' do + pages.reverse_each.with_index do |page_title, index| + expect(page_title.text).to eq(pages_ordered_by_title[index].title) + end + end + end + end + + context 'ordered by created_at' do + let(:pages_ordered_by_created_at) { [wiki_page1, wiki_page2, wiki_page3] } + + before do + page.within('.wiki-sort-dropdown') do + click_button('Title') + click_link('Created date') + end + end + + context 'asc' do + it 'pages are displayed in direct order' do + pages.each.with_index do |page_title, index| + expect(page_title.text).to eq(pages_ordered_by_created_at[index].title) + end + end + end + + context 'desc' do + before do + page.within('.wiki-sort-dropdown') do + page.find('.rspec-reverse-sort').click + end + end + + it 'pages are displayed in reversed order' do + pages.reverse_each.with_index do |page_title, index| + expect(page_title.text).to eq(pages_ordered_by_created_at[index].title) + end + end + end + end +end diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb new file mode 100644 index 00000000000..a7ba7a8ad07 --- /dev/null +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +# Requires a context containing: +# wiki +# user + +RSpec.shared_examples 'User views wiki sidebar' do + include WikiHelpers + + before do + sign_in(user) + end + + context 'when there are some existing pages' do + before do + create(:wiki_page, wiki: wiki, title: 'home', content: 'home') + create(:wiki_page, wiki: wiki, title: 'another', content: 'another') + end + + it 'renders a default sidebar when there is no customized sidebar' do + visit wiki_path(wiki) + + expect(page).to have_content('another') + expect(page).not_to have_link('View All Pages') + end + + context 'when there is a customized sidebar' do + before do + create(:wiki_page, wiki: wiki, title: '_sidebar', content: 'My customized sidebar') + end + + it 'renders my customized sidebar instead of the default one' do + visit wiki_path(wiki) + + expect(page).to have_content('My customized sidebar') + expect(page).not_to have_content('Another') + end + end + end + + context 'when there are 15 existing pages' do + before do + (1..5).each { |i| create(:wiki_page, wiki: wiki, title: "my page #{i}") } + (6..10).each { |i| create(:wiki_page, wiki: wiki, title: "parent/my page #{i}") } + (11..15).each { |i| create(:wiki_page, wiki: wiki, title: "grandparent/parent/my page #{i}") } + end + + it 'shows all pages in the sidebar' do + visit wiki_path(wiki) + + (1..15).each { |i| expect(page).to have_content("my page #{i}") } + expect(page).not_to have_link('View All Pages') + end + + context 'when there are more than 15 existing pages' do + before do + create(:wiki_page, wiki: wiki, title: 'my page 16') + end + + it 'shows the first 15 pages in the sidebar' do + visit wiki_path(wiki) + + expect(page).to have_text('my page', count: 15) + expect(page).to have_link('View All Pages') + end + end + end +end diff --git a/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb new file mode 100644 index 00000000000..ec64519cd9c --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'boards create mutation' do + include GraphqlHelpers + + let_it_be(:current_user, reload: true) { create(:user) } + let(:name) { 'board name' } + let(:mutation) { graphql_mutation(:create_board, params) } + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + def mutation_response + graphql_mutation_response(:create_board) + end + + context 'when the user does not have permission' do + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not create the board' do + expect { subject }.not_to change { Board.count } + end + end + + context 'when the user has permission' do + before do + parent.add_maintainer(current_user) + end + + context 'when the parent (project_path or group_path) param is given' do + context 'when everything is ok' do + it 'creates the board' do + expect { subject }.to change { Board.count }.from(0).to(1) + end + + it 'returns the created board' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response).to have_key('board') + expect(mutation_response['board']['name']).to eq(name) + end + end + + context 'when the Boards::CreateService returns an error response' do + before do + allow_next_instance_of(Boards::CreateService) do |service| + allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'There was an error.')) + end + end + + it 'does not create a board' do + expect { subject }.not_to change { Board.count } + end + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response).to have_key('board') + expect(mutation_response['board']).to be_nil + expect(mutation_response['errors'].first).to eq('There was an error.') + end + end + end + + context 'when neither project_path nor group_path param is given' do + let(:params) { { name: name } } + + it_behaves_like 'a mutation that returns top-level errors', + errors: ['group_path or project_path arguments are required'] + + it 'does not create the board' do + expect { subject }.not_to change { Board.count } + end + end + end +end diff --git a/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb b/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb new file mode 100644 index 00000000000..54b3f84a6e6 --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'spam flag is present' do + specify :aggregate_failures do + subject + + expect(mutation_response).to have_key('spam') + expect(mutation_response['spam']).to be_falsey + end +end + +RSpec.shared_examples 'can raise spam flag' do + it 'spam parameters are passed to the service' do + expect(service).to receive(:new).with(anything, anything, hash_including(api: true, request: instance_of(ActionDispatch::Request))) + + subject + end + + context 'when the snippet is detected as spam' do + it 'raises spam flag' do + allow_next_instance_of(service) do |instance| + allow(instance).to receive(:spam_check) do |snippet, user, _| + snippet.spam! + end + end + + subject + + expect(mutation_response['spam']).to be true + expect(mutation_response['errors']).to include("Your snippet has been recognized as spam and has been discarded.") + end + end + + context 'when :snippet_spam flag is disabled' do + before do + stub_feature_flags(snippet_spam: false) + end + + it 'request parameter is not passed to the service' do + expect(service).to receive(:new).with(anything, anything, hash_not_including(request: instance_of(ActionDispatch::Request))) + + subject + end + end +end diff --git a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb index 522211340ea..24c8a247c93 100644 --- a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb +++ b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb @@ -52,11 +52,15 @@ RSpec.shared_examples 'a Note mutation when the given resource id is not for a N it_behaves_like 'a Note mutation that does not create a Note' - it_behaves_like 'a mutation that returns top-level errors', errors: ['Cannot add notes to this resource'] + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/ does not represent an instance of Noteable/) } + end end RSpec.shared_examples 'a Note mutation when the given resource id is not for a Note' do let(:note) { create(:issue) } - it_behaves_like 'a mutation that returns top-level errors', errors: ['Resource is not a note'] + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/does not represent an instance of Note/) } + end end diff --git a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb index e93077c42e1..7707e79386c 100644 --- a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true -RSpec.shared_examples 'resource mentions migration' do |migration_class, resource_class| +RSpec.shared_examples 'resource mentions migration' do |migration_class, resource_class_name| it 'migrates resource mentions' do join = migration_class::JOIN conditions = migration_class::QUERY_CONDITIONS + resource_class = "#{Gitlab::BackgroundMigration::UserMentions::Models}::#{resource_class_name}".constantize expect do - subject.perform(resource_class.name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id)) + subject.perform(resource_class_name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id)) end.to change { user_mentions.count }.by(1) user_mention = user_mentions.last @@ -16,23 +17,23 @@ RSpec.shared_examples 'resource mentions migration' do |migration_class, resourc # check that performing the same job twice does not fail and does not change counts expect do - subject.perform(resource_class.name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id)) + subject.perform(resource_class_name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id)) end.to change { user_mentions.count }.by(0) end end -RSpec.shared_examples 'resource notes mentions migration' do |migration_class, resource_class| +RSpec.shared_examples 'resource notes mentions migration' do |migration_class, resource_class_name| it 'migrates mentions from note' do join = migration_class::JOIN conditions = migration_class::QUERY_CONDITIONS # there are 5 notes for each noteable_type, but two do not have mentions and # another one's noteable_id points to an inexistent resource - expect(notes.where(noteable_type: resource_class.to_s).count).to eq 5 + expect(notes.where(noteable_type: resource_class_name).count).to eq 5 expect(user_mentions.count).to eq 0 expect do - subject.perform(resource_class.name, join, conditions, true, Note.minimum(:id), Note.maximum(:id)) + subject.perform(resource_class_name, join, conditions, true, Note.minimum(:id), Note.maximum(:id)) end.to change { user_mentions.count }.by(2) # check that the user_mention for regular note is created @@ -51,7 +52,7 @@ RSpec.shared_examples 'resource notes mentions migration' do |migration_class, r # check that performing the same job twice does not fail and does not change counts expect do - subject.perform(resource_class.name, join, conditions, true, Note.minimum(:id), Note.maximum(:id)) + subject.perform(resource_class_name, join, conditions, true, Note.minimum(:id), Note.maximum(:id)) end.to change { user_mentions.count }.by(0) end end @@ -83,24 +84,25 @@ RSpec.shared_examples 'schedules resource mentions migration' do |resource_class end end -RSpec.shared_examples 'resource migration not run' do |migration_class, resource_class| +RSpec.shared_examples 'resource migration not run' do |migration_class, resource_class_name| it 'does not migrate mentions' do join = migration_class::JOIN conditions = migration_class::QUERY_CONDITIONS + resource_class = "#{Gitlab::BackgroundMigration::UserMentions::Models}::#{resource_class_name}".constantize expect do - subject.perform(resource_class.name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id)) + subject.perform(resource_class_name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id)) end.to change { user_mentions.count }.by(0) end end -RSpec.shared_examples 'resource notes migration not run' do |migration_class, resource_class| +RSpec.shared_examples 'resource notes migration not run' do |migration_class, resource_class_name| it 'does not migrate mentions' do join = migration_class::JOIN conditions = migration_class::QUERY_CONDITIONS expect do - subject.perform(resource_class.name, join, conditions, true, Note.minimum(:id), Note.maximum(:id)) + subject.perform(resource_class_name, join, conditions, true, Note.minimum(:id), Note.maximum(:id)) end.to change { user_mentions.count }.by(0) end end diff --git a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb index db5e9461f3f..0df1af3b10a 100644 --- a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb @@ -227,7 +227,7 @@ RSpec.shared_examples 'common trace features' do let(:token) { 'my_secret_token' } before do - build.project.update(runners_token: token) + build.project.update!(runners_token: token) trace.append(token, 0) end @@ -240,7 +240,7 @@ RSpec.shared_examples 'common trace features' do let(:token) { 'my_secret_token' } before do - build.update(token: token) + build.update!(token: token) trace.append(token, 0) end @@ -531,7 +531,7 @@ RSpec.shared_examples 'trace with disabled live trace feature' do context "when erase old trace with 'save'" do before do build.send(:write_attribute, :trace, nil) - build.save + build.save # rubocop:disable Rails/SaveBang end it 'old trace is not deleted' do diff --git a/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb index e43ce936b90..469c0c287b1 100644 --- a/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'diff statistics' do |test_include_stats_flag: true| - subject { described_class.new(diffable, collection_default_args) } + subject { described_class.new(diffable, **collection_default_args) } def stub_stats_find_by_path(path, stats_mock) expect_next_instance_of(Gitlab::Git::DiffStatsCollection) do |collection| diff --git a/spec/support/shared_examples/lib/gitlab/import_export/relation_factory_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/relation_factory_shared_examples.rb new file mode 100644 index 00000000000..33061f17bde --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/import_export/relation_factory_shared_examples.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +# required context: +# - importable: group or project +# - relation_hash: a note relation that's being imported +# - created_object: the object created with the relation factory +RSpec.shared_examples 'Notes user references' do + let(:relation_sym) { :notes } + let(:mapped_user) { create(:user) } + let(:exported_member) do + { + 'id' => 111, + 'access_level' => 30, + 'source_id' => 1, + 'source_type' => importable.class.name == 'Project' ? 'Project' : 'Namespace', + 'user_id' => 3, + 'notification_level' => 3, + 'created_at' => '2016-11-18T09:29:42.634Z', + 'updated_at' => '2016-11-18T09:29:42.634Z', + 'user' => { + 'id' => 999, + 'email' => mapped_user.email, + 'username' => mapped_user.username + } + } + end + + let(:members_mapper) do + Gitlab::ImportExport::MembersMapper.new( + exported_members: [exported_member].compact, + user: importer_user, + importable: importable + ) + end + + shared_examples 'sets the note author to the importer user' do + it { expect(created_object.author).to eq(importer_user) } + end + + shared_examples 'sets the note author to the mapped user' do + it { expect(created_object.author).to eq(mapped_user) } + end + + shared_examples 'does not add original autor note' do + it { expect(created_object.note).not_to include('*By Administrator') } + end + + shared_examples 'adds original autor note' do + it { expect(created_object.note).to include('*By Administrator') } + end + + context 'when the importer is admin' do + let(:importer_user) { create(:admin) } + + context 'and the note author is not mapped' do + let(:exported_member) { nil } + + include_examples 'sets the note author to the importer user' + + include_examples 'adds original autor note' + end + + context 'and the note author is the importer user' do + let(:mapped_user) { importer_user } + + include_examples 'sets the note author to the mapped user' + + include_examples 'does not add original autor note' + end + + context 'and the note author exists in the target instance' do + let(:mapped_user) { create(:user) } + + include_examples 'sets the note author to the mapped user' + + include_examples 'does not add original autor note' + end + end + + context 'when the importer is not admin' do + let(:importer_user) { create(:user) } + + context 'and the note author is not mapped' do + let(:exported_member) { nil } + + include_examples 'sets the note author to the importer user' + + include_examples 'adds original autor note' + end + + context 'and the note author is the importer user' do + let(:mapped_user) { importer_user } + + include_examples 'sets the note author to the importer user' + + include_examples 'adds original autor note' + end + + context 'and the note author exists in the target instance' do + let(:mapped_user) { create(:user) } + + include_examples 'sets the note author to the importer user' + + include_examples 'adds original autor note' + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/repository_size_checker_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/repository_size_checker_shared_examples.rb new file mode 100644 index 00000000000..bb909ffe82a --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/repository_size_checker_shared_examples.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'checker size above limit' do + context 'when size is above the limit' do + let(:current_size) { 100 } + + it 'returns true' do + expect(subject.above_size_limit?).to eq(true) + end + end +end + +RSpec.shared_examples 'checker size not over limit' do + it 'returns false when not over the limit' do + expect(subject.above_size_limit?).to eq(false) + end +end + +RSpec.shared_examples 'checker size exceeded' do + context 'when current size is below or equal to the limit' do + let(:current_size) { 50 } + + it 'returns zero' do + expect(subject.exceeded_size).to eq(0) + end + end + + context 'when current size is over the limit' do + let(:current_size) { 51 } + + it 'returns zero' do + expect(subject.exceeded_size).to eq(1.megabytes) + end + end + + context 'when change size will be over the limit' do + let(:current_size) { 50 } + + it 'returns zero' do + expect(subject.exceeded_size(1.megabytes)).to eq(1.megabytes) + end + end + + context 'when change size will not be over the limit' do + let(:current_size) { 49 } + + it 'returns zero' do + expect(subject.exceeded_size(1.megabytes)).to eq(0) + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/search/recent_items.rb b/spec/support/shared_examples/lib/gitlab/search/recent_items.rb index f96ff4b101e..b3b33e434b9 100644 --- a/spec/support/shared_examples/lib/gitlab/search/recent_items.rb +++ b/spec/support/shared_examples/lib/gitlab/search/recent_items.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true require 'spec_helper' - RSpec.shared_examples 'search recent items' do let_it_be(:user) { create(:user) } - let_it_be(:recent_items) { described_class.new(user: user, items_limit: 5) } - let(:item) { create_item(content: 'hello world 1', project: project) } - let(:project) { create(:project, :public) } + let_it_be(:recent_items) { described_class.new(user: user) } + let(:item) { create_item(content: 'hello world 1', parent: parent) } + let(:parent) { create(parent_type, :public) } describe '#log_view', :clean_gitlab_redis_shared_state do it 'adds the item to the recent items' do @@ -18,13 +17,15 @@ RSpec.shared_examples 'search recent items' do end it 'removes an item when it exceeds the size items_limit' do - (1..6).each do |i| - recent_items.log_view(create_item(content: "item #{i}", project: project)) + recent_items = described_class.new(user: user, items_limit: 3) + + 4.times do |i| + recent_items.log_view(create_item(content: "item #{i}", parent: parent)) end results = recent_items.search('item') - expect(results.map(&:title)).to contain_exactly('item 6', 'item 5', 'item 4', 'item 3', 'item 2') + expect(results.map(&:title)).to contain_exactly('item 3', 'item 2', 'item 1') end it 'expires the items after expires_after' do @@ -39,7 +40,7 @@ RSpec.shared_examples 'search recent items' do it 'does not include results logged for another user' do another_user = create(:user) - another_item = create_item(content: 'hello world 2', project: project) + another_item = create_item(content: 'hello world 2', parent: parent) described_class.new(user: another_user).log_view(another_item) recent_items.log_view(item) @@ -50,11 +51,11 @@ RSpec.shared_examples 'search recent items' do end describe '#search', :clean_gitlab_redis_shared_state do - let(:item1) { create_item(content: "matching item 1", project: project) } - let(:item2) { create_item(content: "matching item 2", project: project) } - let(:item3) { create_item(content: "matching item 3", project: project) } - let(:non_matching_item) { create_item(content: "different item", project: project) } - let!(:non_viewed_item) { create_item(content: "matching but not viewed item", project: project) } + let(:item1) { create_item(content: "matching item 1", parent: parent) } + let(:item2) { create_item(content: "matching item 2", parent: parent) } + let(:item3) { create_item(content: "matching item 3", parent: parent) } + let(:non_matching_item) { create_item(content: "different item", parent: parent) } + let!(:non_viewed_item) { create_item(content: "matching but not viewed item", parent: parent) } before do recent_items.log_view(item1) @@ -74,14 +75,24 @@ RSpec.shared_examples 'search recent items' do end it 'does not leak items you no longer have access to' do - private_project = create(:project, :public, namespace: create(:group)) - private_item = create_item(content: 'matching item title', project: private_project) + private_parent = create(parent_type, :public) + private_item = create_item(content: 'matching item title', parent: private_parent) recent_items.log_view(private_item) - private_project.update!(visibility_level: Project::PRIVATE) + private_parent.update!(visibility_level: ::Gitlab::VisibilityLevel::PRIVATE) expect(recent_items.search('matching')).not_to include(private_item) end + + it "limits results to #{Gitlab::Search::RecentItems::SEARCH_LIMIT} items" do + (Gitlab::Search::RecentItems::SEARCH_LIMIT + 1).times do |i| + recent_items.log_view(create_item(content: "item #{i}", parent: parent)) + end + + results = recent_items.search('item') + + expect(results.count).to eq(Gitlab::Search::RecentItems::SEARCH_LIMIT) + end end end diff --git a/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb new file mode 100644 index 00000000000..d0bef2ad730 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'search results filtered by confidential' do + context 'filter not provided (all behavior)' do + let(:filters) { {} } + + context 'when Feature search_filter_by_confidential enabled' do + it 'returns confidential and not confidential results', :aggregate_failures do + expect(results.objects('issues')).to include confidential_result + expect(results.objects('issues')).to include opened_result + end + end + + context 'when Feature search_filter_by_confidential not enabled' do + before do + stub_feature_flags(search_filter_by_confidential: false) + end + + it 'returns confidential and not confidential results', :aggregate_failures do + expect(results.objects('issues')).to include confidential_result + expect(results.objects('issues')).to include opened_result + end + end + end + + context 'confidential filter' do + let(:filters) { { confidential: true } } + + context 'when Feature search_filter_by_confidential enabled' do + it 'returns only confidential results', :aggregate_failures do + expect(results.objects('issues')).to include confidential_result + expect(results.objects('issues')).not_to include opened_result + end + end + + context 'when Feature search_filter_by_confidential not enabled' do + before do + stub_feature_flags(search_filter_by_confidential: false) + end + + it 'returns confidential and not confidential results', :aggregate_failures do + expect(results.objects('issues')).to include confidential_result + expect(results.objects('issues')).to include opened_result + end + end + end + + context 'not confidential filter' do + let(:filters) { { confidential: false } } + + context 'when Feature search_filter_by_confidential enabled' do + it 'returns not confidential results', :aggregate_failures do + expect(results.objects('issues')).not_to include confidential_result + expect(results.objects('issues')).to include opened_result + end + end + + context 'when Feature search_filter_by_confidential not enabled' do + before do + stub_feature_flags(search_filter_by_confidential: false) + end + + it 'returns confidential and not confidential results', :aggregate_failures do + expect(results.objects('issues')).to include confidential_result + expect(results.objects('issues')).to include opened_result + end + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb new file mode 100644 index 00000000000..765279a78fe --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'search results sorted' do + context 'sort: newest' do + let(:sort) { 'newest' } + + it 'sorts results by created_at' do + expect(results.objects(scope).map(&:id)).to eq([new_result.id, old_result.id, very_old_result.id]) + end + end + + context 'sort: oldest' do + let(:sort) { 'oldest' } + + it 'sorts results by created_at' do + expect(results.objects(scope).map(&:id)).to eq([very_old_result.id, old_result.id, new_result.id]) + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/search_issue_state_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_state_filter_shared_examples.rb index e80ec516407..e80ec516407 100644 --- a/spec/support/shared_examples/lib/gitlab/search_issue_state_filter_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/search_state_filter_shared_examples.rb diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb index 1f5803b90a0..7ce7b2161f6 100644 --- a/spec/support/shared_examples/mailers/notify_shared_examples.rb +++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb @@ -267,3 +267,9 @@ RSpec.shared_examples 'appearance header and footer not enabled' do end end end + +RSpec.shared_examples 'no email is sent' do + it 'does not send an email' do + expect(subject.message).to be_a_kind_of(ActionMailer::Base::NullMail) + end +end diff --git a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb index f37ef3533c3..826ee453919 100644 --- a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb @@ -6,6 +6,14 @@ RSpec.shared_examples 'model with repository' do let(:expected_full_path) { raise NotImplementedError } let(:expected_web_url_path) { expected_full_path } let(:expected_repo_url_path) { expected_full_path } + let(:expected_lfs_enabled) { false } + + it 'container class includes HasRepository' do + # NOTE: This is not enforced at runtime, since we also need to support Geo::DeletedProject + expect(described_class).to include_module(HasRepository) + expect(container).to be_kind_of(HasRepository) + expect(stubbed_container).to be_kind_of(HasRepository) + end describe '#commits_by' do let(:commits) { container.repository.commits('HEAD', limit: 3).commits } @@ -74,6 +82,10 @@ RSpec.shared_examples 'model with repository' do it 'returns valid repo' do expect(container.repository).to be_kind_of(Repository) end + + it 'uses the same container' do + expect(container.repository.container).to be(container) + end end describe '#storage' do @@ -88,6 +100,16 @@ RSpec.shared_examples 'model with repository' do end end + describe '#lfs_enabled?' do + before do + stub_lfs_setting(enabled: true) + end + + it 'returns the expected value' do + expect(container.lfs_enabled?).to eq(expected_lfs_enabled) + end + end + describe '#empty_repo?' do context 'when the repo does not exist' do it 'returns true' do diff --git a/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb b/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb new file mode 100644 index 00000000000..fa929d5b791 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'shardable scopes' do + let_it_be(:secondary_shard) { create(:shard, name: 'test_second_storage') } + + before do + record_2.update!(shard: secondary_shard) + end + + describe '.for_repository_storage' do + it 'returns the objects for a given repository storage' do + expect(described_class.for_repository_storage('default')).to eq([record_1]) + end + end + + describe '.excluding_repository_storage' do + it 'returns the objects excluding the given repository storage' do + expect(described_class.excluding_repository_storage('default')).to eq([record_2]) + end + end +end diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb index d199bae4170..f91e4bd8cf7 100644 --- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb @@ -9,6 +9,11 @@ RSpec.shared_examples 'a timebox' do |timebox_type| let(:user) { create(:user) } let(:timebox_table_name) { timebox_type.to_s.pluralize.to_sym } + # Values implementions can override + let(:mid_point) { Time.now.utc.to_date } + let(:open_on_left) { nil } + let(:open_on_right) { nil } + describe 'modules' do context 'with a project' do it_behaves_like 'AtomicInternalId' do @@ -240,4 +245,85 @@ RSpec.shared_examples 'a timebox' do |timebox_type| expect(timebox.to_ability_name).to eq(timebox_type.to_s) end end + + describe '.within_timeframe' do + let(:factory) { timebox_type } + let(:min_date) { mid_point - 10.days } + let(:max_date) { mid_point + 10.days } + + def box(from, to) + create(factory, *timebox_args, + start_date: from || open_on_left, + due_date: to || open_on_right) + end + + it 'can find overlapping timeboxes' do + fully_open = box(nil, nil) + # ----| ................ # Not overlapping + non_overlapping_open_on_left = box(nil, min_date - 1.day) + # |--| ................ # Not overlapping + non_overlapping_closed_on_left = box(min_date - 2.days, min_date - 1.day) + # ------|............... # Overlapping + overlapping_open_on_left_just = box(nil, min_date) + # -----------------------| # Overlapping + overlapping_open_on_left_fully = box(nil, max_date + 1.day) + # ---------|............ # Overlapping + overlapping_open_on_left_partial = box(nil, min_date + 1.day) + # |-----|............ # Overlapping + overlapping_closed_partial = box(min_date - 1.day, min_date + 1.day) + # |--------------| # Overlapping + exact_match = box(min_date, max_date) + # |--------------------| # Overlapping + larger = box(min_date - 1.day, max_date + 1.day) + # ...|-----|...... # Overlapping + smaller = box(min_date + 1.day, max_date - 1.day) + # .........|-----| # Overlapping + at_end = box(max_date - 1.day, max_date) + # .........|--------- # Overlapping + at_end_open = box(max_date - 1.day, nil) + # |-------------------- # Overlapping + cover_from_left = box(min_date - 1.day, nil) + # .........|--------| # Overlapping + cover_from_middle_closed = box(max_date - 1.day, max_date + 1.day) + # ...............|--| # Overlapping + overlapping_at_end_just = box(max_date, max_date + 1.day) + # ............... |-| # Not Overlapping + not_overlapping_at_right_closed = box(max_date + 1.day, max_date + 2.days) + # ............... |-- # Not Overlapping + not_overlapping_at_right_open = box(max_date + 1.day, nil) + + matches = described_class.within_timeframe(min_date, max_date) + + expect(matches).to include( + overlapping_open_on_left_just, + overlapping_open_on_left_fully, + overlapping_open_on_left_partial, + overlapping_closed_partial, + exact_match, + larger, + smaller, + at_end, + at_end_open, + cover_from_left, + cover_from_middle_closed, + overlapping_at_end_just + ) + + expect(matches).not_to include( + non_overlapping_open_on_left, + non_overlapping_closed_on_left, + not_overlapping_at_right_closed, + not_overlapping_at_right_open + ) + + # Whether we match the 'fully-open' range depends on whether + # it is in fact open (i.e. whether the class allows infinite + # ranges) + if open_on_left.nil? && open_on_right.nil? + expect(matches).not_to include(fully_open) + else + expect(matches).to include(fully_open) + 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 94c52bdaaa6..0ee0b7e6d88 100644 --- a/spec/support/shared_examples/models/mentionable_shared_examples.rb +++ b/spec/support/shared_examples/models/mentionable_shared_examples.rb @@ -207,29 +207,8 @@ RSpec.shared_examples 'an editable mentionable' do end RSpec.shared_examples 'mentions in description' do |mentionable_type| - describe 'when store_mentioned_users_to_db feature disabled' do + describe 'when storing user mentions' do before do - stub_feature_flags(store_mentioned_users_to_db: false) - mentionable.store_mentions! - end - - context 'when mentionable description contains mentions' do - let(:user) { create(:user) } - let(:mentionable) { create(mentionable_type, description: "#{user.to_reference} some description") } - - it 'stores no mentions' do - expect(mentionable.user_mentions.count).to eq 0 - end - - it 'renders description_html correctly' do - expect(mentionable.description_html).to include("<a href=\"/#{user.username}\" data-user=\"#{user.id}\"") - end - end - end - - describe 'when store_mentioned_users_to_db feature enabled' do - before do - stub_feature_flags(store_mentioned_users_to_db: true) mentionable.store_mentions! end diff --git a/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb b/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb index 7701ab42007..66cd8d1df12 100644 --- a/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb +++ b/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb @@ -60,4 +60,20 @@ RSpec.shared_examples 'latest successful build for sha or ref' do expect(subject).to be_nil end end + + context 'with build belonging to a child pipeline' do + let(:child_pipeline) { create_pipeline(project) } + let(:parent_bridge) { create(:ci_bridge, pipeline: pipeline, project: pipeline.project) } + let!(:pipeline_source) { create(:ci_sources_pipeline, source_job: parent_bridge, pipeline: child_pipeline)} + let!(:child_build) { create_build(child_pipeline, 'child-build') } + let(:build_name) { child_build.name } + + before do + child_pipeline.update!(source: :parent_pipeline) + end + + it 'returns the child build' do + expect(subject).to eq(child_build) + end + end end diff --git a/spec/support/shared_examples/models/relative_positioning_shared_examples.rb b/spec/support/shared_examples/models/relative_positioning_shared_examples.rb index d1437244082..b8d12a6da59 100644 --- a/spec/support/shared_examples/models/relative_positioning_shared_examples.rb +++ b/spec/support/shared_examples/models/relative_positioning_shared_examples.rb @@ -31,6 +31,41 @@ RSpec.shared_examples 'a class that supports relative positioning' do end end + def as_item(item) + item # Override to perform a transformation, if necessary + end + + def as_items(items) + items.map { |item| as_item(item) } + end + + describe '#scoped_items' do + it 'includes all items with the same scope' do + scope = as_items([item1, item2, new_item, create_item]) + irrelevant = create(factory, {}) # This should not share the scope + context = RelativePositioning.mover.context(item1) + + same_scope = as_items(context.scoped_items) + + expect(same_scope).to include(*scope) + expect(same_scope).not_to include(as_item(irrelevant)) + end + end + + describe '#relative_siblings' do + it 'includes all items with the same scope, except self' do + scope = as_items([item2, new_item, create_item]) + irrelevant = create(factory, {}) # This should not share the scope + context = RelativePositioning.mover.context(item1) + + siblings = as_items(context.relative_siblings) + + expect(siblings).to include(*scope) + expect(siblings).not_to include(as_item(item1)) + expect(siblings).not_to include(as_item(irrelevant)) + end + end + describe '.move_nulls_to_end' do let(:item3) { create_item } let(:sibling_query) { item1.class.relative_positioning_query_base(item1) } @@ -47,7 +82,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do expect(item1.relative_position).to be(1000) expect(sibling_query.where(relative_position: nil)).not_to exist - expect(sibling_query.reorder(:relative_position, :id)).to eq([item1, item2, item3]) + expect(as_items(sibling_query.reorder(:relative_position, :id))).to eq(as_items([item1, item2, item3])) end it 'preserves relative position' do @@ -117,19 +152,36 @@ RSpec.shared_examples 'a class that supports relative positioning' do expect(bunch.map(&:relative_position)).to all(be < nils.map(&:relative_position).min) end + it 'manages to move nulls found in the relative scope' do + nils = create_items_with_positions([nil] * 4) + + described_class.move_nulls_to_end(sibling_query.to_a) + positions = nils.map { |item| item.reset.relative_position } + + expect(positions).to all(be_present) + expect(positions).to all(be_valid_position) + end + + it 'can move many nulls' do + nils = create_items_with_positions([nil] * 101) + + described_class.move_nulls_to_end(nils) + + expect(nils.map(&:relative_position)).to all(be_valid_position) + end + it 'does not have an N+1 issue' do create_items_with_positions(10..12) - - a, b, c, d, e, f = create_items_with_positions([nil, nil, nil, nil, nil, nil]) + a, b, c, d, e, f, *xs = create_items_with_positions([nil] * 10) baseline = ActiveRecord::QueryRecorder.new do - described_class.move_nulls_to_end([a, e]) + described_class.move_nulls_to_end([a, b]) end - expect { described_class.move_nulls_to_end([b, c, d]) } + expect { described_class.move_nulls_to_end([c, d, e, f]) } .not_to exceed_query_limit(baseline) - expect { described_class.move_nulls_to_end([f]) } + expect { described_class.move_nulls_to_end(xs) } .not_to exceed_query_limit(baseline.count) end end @@ -149,7 +201,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do expect(items.sort_by(&:relative_position)).to eq(items) expect(sibling_query.where(relative_position: nil)).not_to exist - expect(sibling_query.reorder(:relative_position, :id)).to eq(items) + expect(as_items(sibling_query.reorder(:relative_position, :id))).to eq(as_items(items)) expect(item3.relative_position).to be(1000) end @@ -652,3 +704,119 @@ RSpec.shared_examples 'a class that supports relative positioning' do (RelativePositioning::MIN_POSITION..).take(size) end end + +RSpec.shared_examples 'no-op relative positioning' do + def create_item(**params) + create(factory, params.merge(default_params)) + end + + let_it_be(:item1) { create_item } + let_it_be(:item2) { create_item } + let_it_be(:new_item) { create_item(relative_position: nil) } + + def any_relative_positions + new_item.class.reorder(:relative_position, :id).pluck(:id, :relative_position) + end + + shared_examples 'a no-op method' do + it 'does not raise errors' do + expect { perform }.not_to raise_error + end + + it 'does not perform any DB queries' do + expect { perform }.not_to exceed_query_limit(0) + end + + it 'does not change any relative_position' do + expect { perform }.not_to change { any_relative_positions } + end + end + + describe '.scoped_items' do + subject { RelativePositioning.mover.context(item1).scoped_items } + + it 'is empty' do + expect(subject).to be_empty + end + end + + describe '.relative_siblings' do + subject { RelativePositioning.mover.context(item1).relative_siblings } + + it 'is empty' do + expect(subject).to be_empty + end + end + + describe '.move_nulls_to_end' do + subject { item1.class.move_nulls_to_end([new_item, item1]) } + + it_behaves_like 'a no-op method' do + def perform + subject + end + end + + it 'does not move any items' do + expect(subject).to eq(0) + end + end + + describe '.move_nulls_to_start' do + subject { item1.class.move_nulls_to_start([new_item, item1]) } + + it_behaves_like 'a no-op method' do + def perform + subject + end + end + + it 'does not move any items' do + expect(subject).to eq(0) + end + end + + describe 'instance methods' do + subject { new_item } + + describe '#move_to_start' do + it_behaves_like 'a no-op method' do + def perform + subject.move_to_start + end + end + end + + describe '#move_to_end' do + it_behaves_like 'a no-op method' do + def perform + subject.move_to_end + end + end + end + + describe '#move_between' do + it_behaves_like 'a no-op method' do + def perform + subject.move_between(item1, item2) + end + end + end + + describe '#move_before' do + it_behaves_like 'a no-op method' do + def perform + subject.move_before(item1) + end + end + end + + describe '#move_after' do + it_behaves_like 'a no-op method' do + def perform + subject.move_after(item1) + end + end + end + end +end diff --git a/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb b/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb index 07552b62cdd..5198508d48b 100644 --- a/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb +++ b/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb @@ -73,3 +73,13 @@ RSpec.shared_examples 'timebox resource event actions' do end end end + +RSpec.shared_examples 'timebox resource tracks issue metrics' do |type| + describe '#usage_metrics' do + it 'tracks usage' do + expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:"track_issue_#{type}_changed_action") + + create(described_class.name.underscore.to_sym, issue: create(:issue)) + end + end +end diff --git a/spec/support/shared_examples/models/snippet_shared_examples.rb b/spec/support/shared_examples/models/snippet_shared_examples.rb new file mode 100644 index 00000000000..a8fdf9bb81e --- /dev/null +++ b/spec/support/shared_examples/models/snippet_shared_examples.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'size checker for snippet' do |action| + it 'sets up size checker', :aggregate_failures do + expect(checker.current_size).to eq(current_size.megabytes) + expect(checker.limit).to eq(Gitlab::CurrentSettings.snippet_size_limit) + expect(checker.enabled?).to eq(true) + expect(checker.instance_variable_get(:@namespace)).to eq(namespace) + end +end diff --git a/spec/support/shared_examples/models/throttled_touch_shared_examples.rb b/spec/support/shared_examples/models/throttled_touch_shared_examples.rb index 14b851d2828..e869cbce6ae 100644 --- a/spec/support/shared_examples/models/throttled_touch_shared_examples.rb +++ b/spec/support/shared_examples/models/throttled_touch_shared_examples.rb @@ -13,8 +13,8 @@ RSpec.shared_examples 'throttled touch' do first_updated_at = Time.zone.now - (ThrottledTouch::TOUCH_INTERVAL * 2) second_updated_at = Time.zone.now - (ThrottledTouch::TOUCH_INTERVAL * 1.5) - Timecop.freeze(first_updated_at) { subject.touch } - Timecop.freeze(second_updated_at) { subject.touch } + travel_to(first_updated_at) { subject.touch } + travel_to(second_updated_at) { subject.touch } expect(subject.updated_at).to be_like_time(first_updated_at) end diff --git a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb index 557025569b8..7b591ad84d1 100644 --- a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb +++ b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'UpdateProjectStatistics' do +RSpec.shared_examples 'UpdateProjectStatistics' do |with_counter_attribute| let(:project) { subject.project } let(:project_statistics_name) { described_class.project_statistics_name } let(:statistic_attribute) { described_class.statistic_attribute } @@ -13,108 +13,230 @@ RSpec.shared_examples 'UpdateProjectStatistics' do subject.read_attribute(statistic_attribute).to_i end - it { is_expected.to be_new_record } + def read_pending_increment + Gitlab::Redis::SharedState.with do |redis| + key = project.statistics.counter_key(project_statistics_name) + redis.get(key).to_i + end + end - context 'when creating' do - it 'updates the project statistics' do - delta0 = reload_stat + it { is_expected.to be_new_record } - subject.save! + context 'when feature flag efficient_counter_attribute is disabled' do + before do + stub_feature_flags(efficient_counter_attribute: false) + end - delta1 = reload_stat + context 'when creating' do + it 'updates the project statistics' do + delta0 = reload_stat - expect(delta1).to eq(delta0 + read_attribute) - expect(delta1).to be > delta0 - end + subject.save! - it 'schedules a namespace statistics worker' do - expect(Namespaces::ScheduleAggregationWorker) - .to receive(:perform_async).once + delta1 = reload_stat - subject.save! - end - end + expect(delta1).to eq(delta0 + read_attribute) + expect(delta1).to be > delta0 + end - context 'when updating' do - let(:delta) { 42 } + it 'schedules a namespace statistics worker' do + expect(Namespaces::ScheduleAggregationWorker) + .to receive(:perform_async).once - before do - subject.save! + subject.save! + end end - it 'updates project statistics' do - expect(ProjectStatistics) - .to receive(:increment_statistic) - .and_call_original + context 'when updating' do + let(:delta) { 42 } - subject.write_attribute(statistic_attribute, read_attribute + delta) + before do + subject.save! + end - expect { subject.save! } - .to change { reload_stat } - .by(delta) - end + it 'updates project statistics' do + expect(ProjectStatistics) + .to receive(:increment_statistic) + .and_call_original - it 'schedules a namespace statistics worker' do - expect(Namespaces::ScheduleAggregationWorker) - .to receive(:perform_async).once + subject.write_attribute(statistic_attribute, read_attribute + delta) - subject.write_attribute(statistic_attribute, read_attribute + delta) - subject.save! - end + expect { subject.save! } + .to change { reload_stat } + .by(delta) + end - it 'avoids N + 1 queries' do - subject.write_attribute(statistic_attribute, read_attribute + delta) + it 'schedules a namespace statistics worker' do + expect(Namespaces::ScheduleAggregationWorker) + .to receive(:perform_async).once - control_count = ActiveRecord::QueryRecorder.new do + subject.write_attribute(statistic_attribute, read_attribute + delta) subject.save! end - subject.write_attribute(statistic_attribute, read_attribute + delta) + it 'avoids N + 1 queries' do + subject.write_attribute(statistic_attribute, read_attribute + delta) - expect do - subject.save! - end.not_to exceed_query_limit(control_count) - end - end + control_count = ActiveRecord::QueryRecorder.new do + subject.save! + end - context 'when destroying' do - before do - subject.save! + subject.write_attribute(statistic_attribute, read_attribute + delta) + + expect do + subject.save! + end.not_to exceed_query_limit(control_count) + end end - it 'updates the project statistics' do - delta0 = reload_stat + context 'when destroying' do + before do + subject.save! + end - subject.destroy! + it 'updates the project statistics' do + delta0 = reload_stat - delta1 = reload_stat + subject.destroy! - expect(delta1).to eq(delta0 - read_attribute) - expect(delta1).to be < delta0 - end + delta1 = reload_stat + + expect(delta1).to eq(delta0 - read_attribute) + expect(delta1).to be < delta0 + end + + it 'schedules a namespace statistics worker' do + expect(Namespaces::ScheduleAggregationWorker) + .to receive(:perform_async).once - it 'schedules a namespace statistics worker' do - expect(Namespaces::ScheduleAggregationWorker) - .to receive(:perform_async).once + subject.destroy! + end + + context 'when it is destroyed from the project level' do + it 'does not update the project statistics' do + expect(ProjectStatistics) + .not_to receive(:increment_statistic) + + project.update!(pending_delete: true) + project.destroy! + end + + it 'does not schedule a namespace statistics worker' do + expect(Namespaces::ScheduleAggregationWorker) + .not_to receive(:perform_async) - subject.destroy! + project.update!(pending_delete: true) + project.destroy! + end + end end + end - context 'when it is destroyed from the project level' do - it 'does not update the project statistics' do - expect(ProjectStatistics) - .not_to receive(:increment_statistic) + def expect_flush_counter_increments_worker_performed + expect(FlushCounterIncrementsWorker) + .to receive(:perform_in) + .with(CounterAttribute::WORKER_DELAY, project.statistics.class.name, project.statistics.id, project_statistics_name) + expect(FlushCounterIncrementsWorker) + .to receive(:perform_in) + .with(CounterAttribute::WORKER_DELAY, project.statistics.class.name, project.statistics.id, :storage_size) - project.update!(pending_delete: true) - project.destroy! + yield + + # simulate worker running now + expect(Namespaces::ScheduleAggregationWorker).to receive(:perform_async) + FlushCounterIncrementsWorker.new.perform(project.statistics.class.name, project.statistics.id, project_statistics_name) + end + + if with_counter_attribute + context 'when statistic is a counter attribute', :clean_gitlab_redis_shared_state do + context 'when creating' do + it 'stores pending increments for async update' do + initial_stat = reload_stat + expected_increment = read_attribute + + expect_flush_counter_increments_worker_performed do + subject.save! + + expect(read_pending_increment).to eq(expected_increment) + expect(expected_increment).to be > initial_stat + expect(expected_increment).to be_positive + end + end end - it 'does not schedule a namespace statistics worker' do - expect(Namespaces::ScheduleAggregationWorker) - .not_to receive(:perform_async) + context 'when updating' do + let(:delta) { 42 } + + before do + subject.save! + redis_shared_state_cleanup! + end + + it 'stores pending increments for async update' do + expect(ProjectStatistics) + .to receive(:increment_statistic) + .and_call_original + + subject.write_attribute(statistic_attribute, read_attribute + delta) + + expect_flush_counter_increments_worker_performed do + subject.save! + + expect(read_pending_increment).to eq(delta) + end + end + + it 'avoids N + 1 queries' do + subject.write_attribute(statistic_attribute, read_attribute + delta) + + control_count = ActiveRecord::QueryRecorder.new do + subject.save! + end + + subject.write_attribute(statistic_attribute, read_attribute + delta) + + expect do + subject.save! + end.not_to exceed_query_limit(control_count) + end + end - project.update!(pending_delete: true) - project.destroy! + context 'when destroying' do + before do + subject.save! + redis_shared_state_cleanup! + end + + it 'stores pending increment for async update' do + initial_stat = reload_stat + expected_increment = -read_attribute + + expect_flush_counter_increments_worker_performed do + subject.destroy! + + expect(read_pending_increment).to eq(expected_increment) + expect(expected_increment).to be < initial_stat + expect(expected_increment).to be_negative + end + end + + context 'when it is destroyed from the project level' do + it 'does not update the project statistics' do + expect(ProjectStatistics) + .not_to receive(:increment_statistic) + + project.update!(pending_delete: true) + project.destroy! + end + + it 'does not schedule a namespace statistics worker' do + expect(Namespaces::ScheduleAggregationWorker) + .not_to receive(:perform_async) + + project.update!(pending_delete: true) + project.destroy! + end + end end end end diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb index b87f7fe97e1..62da9e15259 100644 --- a/spec/support/shared_examples/models/wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -4,21 +4,99 @@ RSpec.shared_examples 'wiki model' do let_it_be(:user) { create(:user, :commit_email) } let(:wiki_container) { raise NotImplementedError } let(:wiki_container_without_repo) { raise NotImplementedError } + let(:wiki_lfs_enabled) { false } let(:wiki) { described_class.new(wiki_container, user) } let(:commit) { subject.repository.head_commit } subject { wiki } + it 'container class includes HasWiki' do + # NOTE: This is not enforced at runtime, since we also need to support Geo::DeletedProject + expect(wiki_container).to be_kind_of(HasWiki) + expect(wiki_container_without_repo).to be_kind_of(HasWiki) + end + it_behaves_like 'model with repository' do let(:container) { wiki } let(:stubbed_container) { described_class.new(wiki_container_without_repo, user) } let(:expected_full_path) { "#{container.container.full_path}.wiki" } let(:expected_web_url_path) { "#{container.container.web_url(only_path: true).sub(%r{^/}, '')}/-/wikis/home" } + let(:expected_lfs_enabled) { wiki_lfs_enabled } + end + + describe '.container_class' do + it 'is set to the container class' do + expect(described_class.container_class).to eq(wiki_container.class) + end + end + + describe '.find_by_id' do + it 'returns a wiki instance if the container is found' do + wiki = described_class.find_by_id(wiki_container.id) + + expect(wiki).to be_a(described_class) + expect(wiki.container).to eq(wiki_container) + end + + it 'returns nil if the container is not found' do + expect(described_class.find_by_id(-1)).to be_nil + end + end + + describe '#initialize' do + it 'accepts a valid user' do + expect do + described_class.new(wiki_container, user) + end.not_to raise_error + end + + it 'accepts a blank user' do + expect do + described_class.new(wiki_container, nil) + end.not_to raise_error + end + + it 'raises an error for invalid users' do + expect do + described_class.new(wiki_container, Object.new) + end.to raise_error(ArgumentError, 'user must be a User, got Object') + end + end + + describe '#run_after_commit' do + it 'delegates to the container' do + expect(wiki_container).to receive(:run_after_commit) + + wiki.run_after_commit + end + end + + describe '#==' do + it 'returns true for wikis from the same container' do + expect(wiki).to eq(described_class.new(wiki_container)) + end + + it 'returns false for wikis from different containers' do + expect(wiki).not_to eq(described_class.new(wiki_container_without_repo)) + end + end + + describe '#id' do + it 'returns the ID of the container' do + expect(wiki.id).to eq(wiki_container.id) + end + end + + describe '#to_global_id' do + it 'returns a global ID' do + expect(wiki.to_global_id.to_s).to eq("gid://gitlab/#{wiki.class.name}/#{wiki.id}") + end end describe '#repository' do it 'returns a wiki repository' do expect(subject.repository.repo_type).to be_wiki + expect(subject.repository.container).to be(subject) end end @@ -164,7 +242,7 @@ RSpec.shared_examples 'wiki model' do def total_pages(entries) entries.sum do |entry| - entry.is_a?(WikiDirectory) ? entry.pages.size : 1 + entry.is_a?(WikiDirectory) ? total_pages(entry.entries) : 1 end end @@ -204,8 +282,9 @@ RSpec.shared_examples 'wiki model' do expect(page.title).to eq('index page') end - it 'returns nil if the page does not exist' do - expect(subject.find_page('non-existent')).to eq(nil) + it 'returns nil if the page or version does not exist' do + expect(subject.find_page('non-existent')).to be_nil + expect(subject.find_page('index page', 'non-existent')).to be_nil end it 'can find a page by slug' do diff --git a/spec/support/shared_examples/policies/resource_access_token_shared_examples.rb b/spec/support/shared_examples/policies/resource_access_token_shared_examples.rb new file mode 100644 index 00000000000..7710e756e5b --- /dev/null +++ b/spec/support/shared_examples/policies/resource_access_token_shared_examples.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'Self-managed Core resource access tokens' do + before do + allow(::Gitlab).to receive(:com?).and_return(false) + end + + context 'with owner' do + let(:current_user) { owner } + + it { is_expected.to be_allowed(:admin_resource_access_tokens) } + end + + context 'with developer' do + let(:current_user) { developer } + + it { is_expected.not_to be_allowed(:admin_resource_access_tokens) } + end +end + +RSpec.shared_examples 'GitLab.com Core resource access tokens' do + before do + allow(::Gitlab).to receive(:com?).and_return(true) + stub_ee_application_setting(should_check_namespace_plan: true) + end + + context 'with owner' do + let(:current_user) { owner } + + it { is_expected.not_to be_allowed(:admin_resource_access_tokens) } + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb index 50a8b81b518..3cdba315d1f 100644 --- a/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb @@ -109,7 +109,7 @@ RSpec.shared_examples 'issuable quick actions' do QuickAction.new( action_text: "/unlock", before_action: -> { - issuable.update(discussion_locked: true) + issuable.update!(discussion_locked: true) }, expectation: ->(noteable, can_use_quick_action) { if can_use_quick_action @@ -128,7 +128,7 @@ RSpec.shared_examples 'issuable quick actions' do QuickAction.new( action_text: "/remove_milestone", before_action: -> { - issuable.update(milestone_id: milestone.id) + issuable.update!(milestone_id: milestone.id) }, expectation: ->(noteable, can_use_quick_action) { if can_use_quick_action @@ -171,7 +171,7 @@ RSpec.shared_examples 'issuable quick actions' do QuickAction.new( action_text: "/remove_estimate", before_action: -> { - issuable.update(time_estimate: 30000) + issuable.update!(time_estimate: 30000) }, expectation: ->(noteable, can_use_quick_action) { if can_use_quick_action @@ -228,7 +228,7 @@ RSpec.shared_examples 'issuable quick actions' do before do project.add_developer(old_assignee) - issuable.update(assignees: [old_assignee]) + issuable.update!(assignees: [old_assignee]) end context 'when user can update issuable' do diff --git a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb index 258d9ab85e4..acbc6429646 100644 --- a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb @@ -52,7 +52,7 @@ RSpec.shared_examples 'merge quick action' do context 'when the head diff changes in the meanwhile' do before do merge_request.source_branch = 'another_branch' - merge_request.save + merge_request.save! sign_in(user) visit project_merge_request_path(project, merge_request) end diff --git a/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb index 5c122b4b5d6..4b5299cebec 100644 --- a/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb @@ -75,7 +75,7 @@ RSpec.shared_examples 'Composer package creation' do |user_type, status, add_mem expect(response).to have_gitlab_http_status(status) end - it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package' + it_behaves_like 'a package tracking event', described_class.name, 'push_package' end end diff --git a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb index 3e058838773..e776cf13217 100644 --- a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb @@ -79,11 +79,3 @@ RSpec.shared_examples 'returns repositories for allowed users' do |user_type, sc end end end - -RSpec.shared_examples 'a gitlab tracking event' do |category, action| - it "creates a gitlab tracking event #{action}" do - expect(Gitlab::Tracking).to receive(:event).with(category, action, {}) - - subject - end -end diff --git a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb new file mode 100644 index 00000000000..ec32cb4b2ff --- /dev/null +++ b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb @@ -0,0 +1,309 @@ +# frozen_string_literal: true + +RSpec.shared_context 'Debian repository shared context' do |object_type| + before do + stub_feature_flags(debian_packages: true) + end + + if object_type == :project + let(:project) { create(:project, :public) } + elsif object_type == :group + let(:group) { create(:group, :public) } + end + + let(:user) { create(:user) } + let(:personal_access_token) { create(:personal_access_token, user: user) } + + let(:distribution) { 'bullseye' } + let(:component) { 'main' } + let(:architecture) { 'amd64' } + let(:source_package) { 'sample' } + let(:letter) { source_package[0..2] == 'lib' ? source_package[0..3] : source_package[0] } + let(:package_name) { 'libsample0' } + let(:package_version) { '1.2.3~alpha2-1' } + let(:file_name) { "#{package_name}_#{package_version}_#{architecture}.deb" } + + let(:method) { :get } + + let(:workhorse_params) do + if method == :put + file_upload = fixture_file_upload("spec/fixtures/packages/debian/#{file_name}") + { file: file_upload } + else + {} + end + end + + let(:params) { workhorse_params } + + let(:auth_headers) { {} } + let(:workhorse_headers) do + if method == :put + workhorse_token = JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') + { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } + else + {} + end + end + + let(:headers) { auth_headers.merge(workhorse_headers) } + + let(:send_rewritten_field) { true } + + subject do + if method == :put + workhorse_finalize( + api(url), + method: method, + file_key: :file, + params: params, + headers: headers, + send_rewritten_field: send_rewritten_field + ) + else + send method, api(url), headers: headers, params: params + end + end +end + +RSpec.shared_context 'Debian repository auth headers' do |user_role, user_token, auth_method = :token| + let(:token) { user_token ? personal_access_token.token : 'wrong' } + + let(:auth_headers) do + if user_role == :anonymous + {} + elsif auth_method == :token + { 'Private-Token' => token } + else + basic_auth_header(user.username, token) + end + end +end + +RSpec.shared_context 'Debian repository project access' do |project_visibility_level, user_role, user_token, auth_method| + include_context 'Debian repository auth headers', user_role, user_token, auth_method do + before do + project.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + end +end + +RSpec.shared_examples 'Debian project repository GET request' do |user_role, add_member, status, body| + context "for user type #{user_role}" do + before do + project.send("add_#{user_role}", user) if add_member && user_role != :anonymous + end + + and_body = body.nil? ? '' : ' and expected body' + + it "returns #{status}#{and_body}" do + subject + + expect(response).to have_gitlab_http_status(status) + + unless body.nil? + expect(response.body).to eq(body) + end + end + end +end + +RSpec.shared_examples 'Debian project repository PUT request' do |user_role, add_member, status, body| + context "for user type #{user_role}" do + before do + project.send("add_#{user_role}", user) if add_member && user_role != :anonymous + end + + and_body = body.nil? ? '' : ' and expected body' + + if status == :created + it 'creates package files' do + pending "Debian package creation not implemented" + expect { subject } + .to change { project.packages.debian.count }.by(1) + + expect(response).to have_gitlab_http_status(status) + + unless body.nil? + expect(response.body).to eq(body) + end + end + it_behaves_like 'a package tracking event', described_class.name, 'push_package' + else + it "returns #{status}#{and_body}" do + subject + + expect(response).to have_gitlab_http_status(status) + + unless body.nil? + expect(response.body).to eq(body) + end + end + end + end +end + +RSpec.shared_examples 'rejects Debian access with unknown project id' do + context 'with an unknown project' do + let(:project) { double(id: non_existing_record_id) } + + context 'as anonymous' do + it_behaves_like 'Debian project repository GET request', :anonymous, true, :not_found, nil + end + + context 'as authenticated user' do + subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } + + it_behaves_like 'Debian project repository GET request', :anonymous, true, :not_found, nil + end + end +end + +RSpec.shared_examples 'Debian project repository GET endpoint' do |success_status, success_body| + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do + 'PUBLIC' | :developer | true | true | success_status | success_body + 'PUBLIC' | :guest | true | true | success_status | success_body + 'PUBLIC' | :developer | true | false | success_status | success_body + 'PUBLIC' | :guest | true | false | success_status | success_body + 'PUBLIC' | :developer | false | true | success_status | success_body + 'PUBLIC' | :guest | false | true | success_status | success_body + 'PUBLIC' | :developer | false | false | success_status | success_body + 'PUBLIC' | :guest | false | false | success_status | success_body + 'PUBLIC' | :anonymous | false | true | success_status | success_body + 'PRIVATE' | :developer | true | true | success_status | success_body + 'PRIVATE' | :guest | true | true | :forbidden | nil + 'PRIVATE' | :developer | true | false | :not_found | nil + 'PRIVATE' | :guest | true | false | :not_found | nil + 'PRIVATE' | :developer | false | true | :not_found | nil + 'PRIVATE' | :guest | false | true | :not_found | nil + 'PRIVATE' | :developer | false | false | :not_found | nil + 'PRIVATE' | :guest | false | false | :not_found | nil + 'PRIVATE' | :anonymous | false | true | :not_found | nil + end + + with_them do + include_context 'Debian repository project access', params[:project_visibility_level], params[:user_role], params[:user_token], :basic do + it_behaves_like 'Debian project repository GET request', params[:user_role], params[:member], params[:expected_status], params[:expected_body] + end + end + end + + it_behaves_like 'rejects Debian access with unknown project id' +end + +RSpec.shared_examples 'Debian project repository PUT endpoint' do |success_status, success_body| + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do + 'PUBLIC' | :developer | true | true | success_status | nil + 'PUBLIC' | :guest | true | true | :forbidden | nil + 'PUBLIC' | :developer | true | false | :unauthorized | nil + 'PUBLIC' | :guest | true | false | :unauthorized | nil + 'PUBLIC' | :developer | false | true | :forbidden | nil + 'PUBLIC' | :guest | false | true | :forbidden | nil + 'PUBLIC' | :developer | false | false | :unauthorized | nil + 'PUBLIC' | :guest | false | false | :unauthorized | nil + 'PUBLIC' | :anonymous | false | true | :unauthorized | nil + 'PRIVATE' | :developer | true | true | success_status | nil + 'PRIVATE' | :guest | true | true | :forbidden | nil + 'PRIVATE' | :developer | true | false | :not_found | nil + 'PRIVATE' | :guest | true | false | :not_found | nil + 'PRIVATE' | :developer | false | true | :not_found | nil + 'PRIVATE' | :guest | false | true | :not_found | nil + 'PRIVATE' | :developer | false | false | :not_found | nil + 'PRIVATE' | :guest | false | false | :not_found | nil + 'PRIVATE' | :anonymous | false | true | :not_found | nil + end + + with_them do + include_context 'Debian repository project access', params[:project_visibility_level], params[:user_role], params[:user_token], :basic do + it_behaves_like 'Debian project repository PUT request', params[:user_role], params[:member], params[:expected_status], params[:expected_body] + end + end + end + + it_behaves_like 'rejects Debian access with unknown project id' +end + +RSpec.shared_context 'Debian repository group access' do |group_visibility_level, user_role, user_token, auth_method| + include_context 'Debian repository auth headers', user_role, user_token, auth_method do + before do + group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility_level, false)) + end + end +end + +RSpec.shared_examples 'Debian group repository GET request' do |user_role, add_member, status, body| + context "for user type #{user_role}" do + before do + group.send("add_#{user_role}", user) if add_member && user_role != :anonymous + end + + and_body = body.nil? ? '' : ' and expected body' + + it "returns #{status}#{and_body}" do + subject + + expect(response).to have_gitlab_http_status(status) + + unless body.nil? + expect(response.body).to eq(body) + end + end + end +end + +RSpec.shared_examples 'rejects Debian access with unknown group id' do + context 'with an unknown group' do + let(:group) { double(id: non_existing_record_id) } + + context 'as anonymous' do + it_behaves_like 'Debian group repository GET request', :anonymous, true, :not_found, nil + end + + context 'as authenticated user' do + subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } + + it_behaves_like 'Debian group repository GET request', :anonymous, true, :not_found, nil + end + end +end + +RSpec.shared_examples 'Debian group repository GET endpoint' do |success_status, success_body| + context 'with valid group' do + using RSpec::Parameterized::TableSyntax + + where(:group_visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do + 'PUBLIC' | :developer | true | true | success_status | success_body + 'PUBLIC' | :guest | true | true | success_status | success_body + 'PUBLIC' | :developer | true | false | success_status | success_body + 'PUBLIC' | :guest | true | false | success_status | success_body + 'PUBLIC' | :developer | false | true | success_status | success_body + 'PUBLIC' | :guest | false | true | success_status | success_body + 'PUBLIC' | :developer | false | false | success_status | success_body + 'PUBLIC' | :guest | false | false | success_status | success_body + 'PUBLIC' | :anonymous | false | true | success_status | success_body + 'PRIVATE' | :developer | true | true | success_status | success_body + 'PRIVATE' | :guest | true | true | :forbidden | nil + 'PRIVATE' | :developer | true | false | :not_found | nil + 'PRIVATE' | :guest | true | false | :not_found | nil + 'PRIVATE' | :developer | false | true | :not_found | nil + 'PRIVATE' | :guest | false | true | :not_found | nil + 'PRIVATE' | :developer | false | false | :not_found | nil + 'PRIVATE' | :guest | false | false | :not_found | nil + 'PRIVATE' | :anonymous | false | true | :not_found | nil + end + + with_them do + include_context 'Debian repository group access', params[:group_visibility_level], params[:user_role], params[:user_token], :basic do + it_behaves_like 'Debian group repository GET request', params[:user_role], params[:member], params[:expected_status], params[:expected_body] + end + end + end + + it_behaves_like 'rejects Debian access with unknown group id' +end diff --git a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb index f26af6cb766..5145880ef9a 100644 --- a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb @@ -90,7 +90,7 @@ RSpec.shared_examples 'group and project boards query' do it_behaves_like 'a working graphql query' do before do - post_graphql(query_single_board, current_user: current_user) + post_graphql(query_single_board("id: \"gid://gitlab/Board/1\""), current_user: current_user) end end diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb index 6aac51a5903..58e99776fd9 100644 --- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb @@ -26,7 +26,7 @@ RSpec.shared_examples 'process nuget service index request' do |user_type, statu it_behaves_like 'returning response status', status - it_behaves_like 'a gitlab tracking event', described_class.name, 'nuget_service_index' + it_behaves_like 'a package tracking event', described_class.name, 'cli_metadata' it 'returns a valid json response' do subject @@ -169,7 +169,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = context 'with correct params' do it_behaves_like 'package workhorse uploads' it_behaves_like 'creates nuget package files' - it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package' + it_behaves_like 'a package tracking event', described_class.name, 'push_package' end end @@ -286,7 +286,7 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st it_behaves_like 'returning response status', status - it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package' + it_behaves_like 'a package tracking event', described_class.name, 'pull_package' it 'returns a valid package archive' do subject @@ -336,7 +336,7 @@ RSpec.shared_examples 'process nuget search request' do |user_type, status, add_ it_behaves_like 'returns a valid json search response', status, 4, [1, 5, 5, 1] - it_behaves_like 'a gitlab tracking event', described_class.name, 'search_package' + it_behaves_like 'a package tracking event', described_class.name, 'search_package' context 'with skip set to 2' do let(:skip) { 2 } 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 c9a33701161..d730ed53109 100644 --- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb @@ -126,3 +126,11 @@ RSpec.shared_examples 'job token for package uploads' do end end end + +RSpec.shared_examples 'a package tracking event' do |category, action| + it "creates a gitlab tracking event #{action}" do + expect(Gitlab::Tracking).to receive(:event).with(category, action, {}) + + expect { subject }.to change { Packages::Event.count }.by(1) + end +end diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb index 715c494840e..bbcf856350d 100644 --- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'PyPi package creation' do |user_type, status, add_member = true| +RSpec.shared_examples 'PyPI package creation' do |user_type, status, add_member = true| RSpec.shared_examples 'creating pypi package files' do it 'creates package files' do expect { subject } @@ -52,7 +52,7 @@ RSpec.shared_examples 'PyPi package creation' do |user_type, status, add_member context 'with correct params' do it_behaves_like 'package workhorse uploads' it_behaves_like 'creating pypi package files' - it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package' + it_behaves_like 'a package tracking event', described_class.name, 'push_package' end end @@ -106,7 +106,7 @@ RSpec.shared_examples 'PyPi package creation' do |user_type, status, add_member end end -RSpec.shared_examples 'PyPi package versions' do |user_type, status, add_member = true| +RSpec.shared_examples 'PyPI package versions' do |user_type, status, add_member = true| context "for user type #{user_type}" do before do project.send("add_#{user_type}", user) if add_member && user_type != :anonymous @@ -119,11 +119,11 @@ RSpec.shared_examples 'PyPi package versions' do |user_type, status, add_member end it_behaves_like 'returning response status', status - it_behaves_like 'a gitlab tracking event', described_class.name, 'list_package' + it_behaves_like 'a package tracking event', described_class.name, 'list_package' end end -RSpec.shared_examples 'PyPi package download' do |user_type, status, add_member = true| +RSpec.shared_examples 'PyPI package download' do |user_type, status, add_member = true| context "for user type #{user_type}" do before do project.send("add_#{user_type}", user) if add_member && user_type != :anonymous @@ -136,11 +136,11 @@ RSpec.shared_examples 'PyPi package download' do |user_type, status, add_member end it_behaves_like 'returning response status', status - it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package' + it_behaves_like 'a package tracking event', described_class.name, 'pull_package' end end -RSpec.shared_examples 'process PyPi api request' do |user_type, status, add_member = true| +RSpec.shared_examples 'process PyPI api request' do |user_type, status, add_member = true| context "for user type #{user_type}" do before do project.send("add_#{user_type}", user) if add_member && user_type != :anonymous @@ -155,13 +155,13 @@ RSpec.shared_examples 'rejects PyPI access with unknown project id' do let(:project) { OpenStruct.new(id: 1234567890) } context 'as anonymous' do - it_behaves_like 'process PyPi api request', :anonymous, :not_found + it_behaves_like 'process PyPI api request', :anonymous, :not_found end context 'as authenticated user' do subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } - it_behaves_like 'process PyPi api request', :anonymous, :not_found + it_behaves_like 'process PyPI api request', :anonymous, :not_found end end end diff --git a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb index 051367fbe96..2b72c69cb37 100644 --- a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb @@ -1,46 +1,30 @@ # frozen_string_literal: true RSpec.shared_examples 'raw snippet files' do - let_it_be(:unauthorized_user) { create(:user) } + let_it_be(:user_token) { create(:personal_access_token, user: snippet.author) } let(:snippet_id) { snippet.id } let(:user) { snippet.author } let(:file_path) { '%2Egitattributes' } let(:ref) { 'master' } - context 'with no user' do - it 'requires authentication' do - get api(api_path) + subject { get api(api_path, personal_access_token: user_token) } - expect(response).to have_gitlab_http_status(:unauthorized) - end - end + context 'with an invalid snippet ID' do + let(:snippet_id) { non_existing_record_id } - shared_examples 'not found' do it 'returns 404' do - get api(api_path, user) + subject expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Snippet Not Found') end end - context 'when not authorized' do - let(:user) { unauthorized_user } - - it_behaves_like 'not found' - end - - context 'with an invalid snippet ID' do - let(:snippet_id) { 'invalid' } - - it_behaves_like 'not found' - end - context 'with valid params' do it 'returns the raw file info' do expect(Gitlab::Workhorse).to receive(:send_git_blob).and_call_original - get api(api_path, user) + subject aggregate_failures do expect(response).to have_gitlab_http_status(:ok) @@ -52,6 +36,17 @@ RSpec.shared_examples 'raw snippet files' do end end + context 'with unauthorized user' do + let(:user_token) { create(:personal_access_token) } + + it 'returns 404' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + end + context 'with invalid params' do using RSpec::Parameterized::TableSyntax @@ -68,12 +63,12 @@ RSpec.shared_examples 'raw snippet files' do end with_them do - before do - get api(api_path, user) - end + it 'returns the proper response code and message' do + subject - it { expect(response).to have_gitlab_http_status(status) } - it { expect(json_response[key]).to eq(message) } + expect(response).to have_gitlab_http_status(status) + expect(json_response[key]).to eq(message) + end end end end @@ -216,3 +211,133 @@ RSpec.shared_examples 'invalid snippet updates' do expect(json_response['error']).to eq 'title is empty' end end + +RSpec.shared_examples 'snippet access with different users' do + using RSpec::Parameterized::TableSyntax + + where(:requester, :visibility, :status) do + :admin | :public | :ok + :admin | :private | :ok + :admin | :internal | :ok + :author | :public | :ok + :author | :private | :ok + :author | :internal | :ok + :other | :public | :ok + :other | :private | :not_found + :other | :internal | :ok + nil | :public | :ok + nil | :private | :not_found + nil | :internal | :not_found + end + + with_them do + let(:snippet) { snippet_for(visibility) } + + it 'returns the correct response' do + request_user = user_for(requester) + + get api(path, request_user) + + expect(response).to have_gitlab_http_status(status) + end + end + + def user_for(user_type) + case user_type + when :author + user + when :other + other_user + when :admin + admin + else + nil + end + end + + def snippet_for(snippet_type) + case snippet_type + when :private + private_snippet + when :internal + internal_snippet + when :public + public_snippet + end + end +end + +RSpec.shared_examples 'expected response status' do + it 'returns the correct response' do + get api(path, personal_access_token: user_token) + + expect(response).to have_gitlab_http_status(status) + end +end + +RSpec.shared_examples 'unauthenticated project snippet access' do + using RSpec::Parameterized::TableSyntax + + let(:user_token) { nil } + + where(:project_visibility, :snippet_visibility, :status) do + :public | :public | :ok + :public | :private | :not_found + :public | :internal | :not_found + :internal | :public | :not_found + :private | :public | :not_found + end + + with_them do + it_behaves_like 'expected response status' + end +end + +RSpec.shared_examples 'non-member project snippet access' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility, :snippet_visibility, :status) do + :public | :public | :ok + :public | :internal | :ok + :internal | :public | :ok + :public | :private | :not_found + :private | :public | :not_found + end + + with_them do + it_behaves_like 'expected response status' + end +end + +RSpec.shared_examples 'member project snippet access' do + using RSpec::Parameterized::TableSyntax + + before do + project.add_guest(user) + end + + where(:project_visibility, :snippet_visibility, :status) do + :public | :public | :ok + :public | :internal | :ok + :internal | :public | :ok + :public | :private | :ok + :private | :public | :ok + end + + with_them do + it_behaves_like 'expected response status' + end +end + +RSpec.shared_examples 'project snippet access levels' do + let_it_be(:user_token) { create(:personal_access_token, user: user) } + + let(:project) { create(:project, project_visibility) } + let(:snippet) { create(:project_snippet, :repository, snippet_visibility, project: project) } + + it_behaves_like 'unauthenticated project snippet access' + + it_behaves_like 'non-member project snippet access' + + it_behaves_like 'member project snippet access' +end diff --git a/spec/support/shared_examples/requests/api/tracking_shared_examples.rb b/spec/support/shared_examples/requests/api/tracking_shared_examples.rb new file mode 100644 index 00000000000..2e6feae3f98 --- /dev/null +++ b/spec/support/shared_examples/requests/api/tracking_shared_examples.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a gitlab tracking event' do |category, action| + it "creates a gitlab tracking event #{action}" do + expect(Gitlab::Tracking).to receive(:event).with(category, action, {}) + + subject + 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 08ccbd4a9c1..730df4dc5ab 100644 --- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb +++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb @@ -48,7 +48,7 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do expect_rejection { make_request(request_args) } - Timecop.travel(period.from_now) do + travel_to(period.from_now) do requests_per_period.times do make_request(request_args) expect(response).not_to have_gitlab_http_status(:too_many_requests) @@ -175,7 +175,7 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do expect_rejection { request_authenticated_web_url } - Timecop.travel(period.from_now) do + travel_to(period.from_now) do requests_per_period.times do request_authenticated_web_url expect(response).not_to have_gitlab_http_status(:too_many_requests) diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb index 84ef7723b9b..dae3a3e74be 100644 --- a/spec/support/shared_examples/requests/snippet_shared_examples.rb +++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb @@ -99,18 +99,6 @@ RSpec.shared_examples 'snippet blob content' do end end -RSpec.shared_examples 'snippet_multiple_files feature disabled' do - before do - stub_feature_flags(snippet_multiple_files: false) - - subject - end - - it 'does not return files attributes' do - expect(json_response).not_to have_key('files') - end -end - RSpec.shared_examples 'snippet creation with files parameter' do using RSpec::Parameterized::TableSyntax diff --git a/spec/support/shared_examples/requests/user_activity_shared_examples.rb b/spec/support/shared_examples/requests/user_activity_shared_examples.rb new file mode 100644 index 00000000000..37da1ce5c63 --- /dev/null +++ b/spec/support/shared_examples/requests/user_activity_shared_examples.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'updating of user activity' do |paths_to_visit| + let(:user) { create(:user, last_activity_on: nil) } + + before do + group = create(:group, name: 'group') + project = create(:project, :public, namespace: group, name: 'project') + + create(:issue, project: project, iid: 10) + create(:merge_request, source_project: project, iid: 15) + + project.add_maintainer(user) + end + + context 'without an authenticated user' do + it 'does not set the last activity cookie' do + get "/group/project" + + expect(response.cookies['user_last_activity_on']).to be_nil + end + end + + context 'with an authenticated user' do + before do + login_as(user) + end + + context 'with a POST request' do + it 'does not set the last activity cookie' do + post "/group/project/archive" + + expect(response.cookies['user_last_activity_on']).to be_nil + end + end + + paths_to_visit.each do |path| + context "on GET to #{path}" do + it 'updates the last activity date' do + expect(Users::ActivityService).to receive(:new).and_call_original + + get path + + expect(user.last_activity_on).to eq(Date.today) + end + + context 'when calling it twice' do + it 'updates last_activity_on just once' do + expect(Users::ActivityService).to receive(:new).once.and_call_original + + 2.times do + get path + end + end + end + + context 'when last_activity_on is nil' do + before do + user.update_attribute(:last_activity_on, nil) + end + + it 'updates the last activity date' do + expect(user.last_activity_on).to be_nil + + get path + + expect(user.last_activity_on).to eq(Date.today) + end + end + + context 'when last_activity_on is stale' do + before do + user.update_attribute(:last_activity_on, 2.days.ago.to_date) + end + + it 'updates the last activity date' do + get path + + expect(user.last_activity_on).to eq(Date.today) + end + end + + context 'when last_activity_on is up to date' do + before do + user.update_attribute(:last_activity_on, Date.today) + end + + it 'does not try to update it' do + expect(Users::ActivityService).not_to receive(:new) + + get path + end + end + end + end + end +end diff --git a/spec/support/shared_examples/serializers/note_entity_shared_examples.rb b/spec/support/shared_examples/serializers/note_entity_shared_examples.rb index 7b2ec02c7b6..a90a2dc3667 100644 --- a/spec/support/shared_examples/serializers/note_entity_shared_examples.rb +++ b/spec/support/shared_examples/serializers/note_entity_shared_examples.rb @@ -24,7 +24,7 @@ RSpec.shared_examples 'note entity' do context 'when note was edited' do before do - note.update(updated_at: 1.minute.from_now, updated_by: user) + note.update!(updated_at: 1.minute.from_now, updated_by: user) end it 'exposes last_edited_at and last_edited_by elements' do @@ -34,7 +34,7 @@ RSpec.shared_examples 'note entity' do context 'when note is a system note' do before do - note.update(system: true) + note.update!(system: true) end it 'exposes system_note_icon_name element' do diff --git a/spec/support/shared_examples/services/boards/boards_create_service_shared_examples.rb b/spec/support/shared_examples/services/boards/boards_create_service_shared_examples.rb index fced2e59ace..f28c78aec97 100644 --- a/spec/support/shared_examples/services/boards/boards_create_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/boards_create_service_shared_examples.rb @@ -7,7 +7,7 @@ RSpec.shared_examples 'boards create service' do end it 'creates the default lists' do - board = service.execute + board = service.execute.payload expect(board.lists.size).to eq 2 expect(board.lists.first).to be_backlog diff --git a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb index f352b430cc7..4aa5d7d890b 100644 --- a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb @@ -131,7 +131,7 @@ RSpec.shared_examples 'issues move service' do |group| updated_at1 = issue1.updated_at updated_at2 = issue2.updated_at - Timecop.travel(1.minute.from_now) do + travel_to(1.minute.from_now) do described_class.new(parent, user, params).execute(issue) end diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb index d6e79931df5..39c22ac8aa3 100644 --- a/spec/support/shared_examples/services/incident_shared_examples.rb +++ b/spec/support/shared_examples/services/incident_shared_examples.rb @@ -45,3 +45,74 @@ RSpec.shared_examples 'not an incident issue' do expect(issue.labels).not_to include(have_attributes(label_properties)) end end + +# This shared example is to test the execution of incident management label services +# For example: +# - IncidentManagement::CreateIncidentSlaExceededLabelService +# - IncidentManagement::CreateIncidentLabelService + +# It doesn't require any defined variables + +RSpec.shared_examples 'incident management label service' do + let_it_be(:project) { create(:project, :private) } + let_it_be(:user) { User.alert_bot } + let(:service) { described_class.new(project, user) } + + subject(:execute) { service.execute } + + describe 'execute' do + let(:incident_label_attributes) { described_class::LABEL_PROPERTIES } + let(:title) { incident_label_attributes[:title] } + let(:color) { incident_label_attributes[:color] } + let(:description) { incident_label_attributes[:description] } + + shared_examples 'existing label' do + it 'returns the existing label' do + expect { execute }.not_to change(Label, :count) + + expect(execute).to be_success + expect(execute.payload).to eq(label: label) + end + end + + shared_examples 'new label' do + it 'creates a new label' do + expect { execute }.to change(Label, :count).by(1) + + label = project.reload.labels.last + expect(execute).to be_success + expect(execute.payload).to eq(label: label) + expect(label.title).to eq(title) + expect(label.color).to eq(color) + expect(label.description).to eq(description) + end + end + + context 'with predefined project label' do + it_behaves_like 'existing label' do + let!(:label) { create(:label, project: project, title: title) } + end + end + + context 'with predefined group label' do + let(:project) { create(:project, group: group) } + let(:group) { create(:group) } + + it_behaves_like 'existing label' do + let!(:label) { create(:group_label, group: group, title: title) } + end + end + + context 'without label' do + context 'when user has permissions to create labels' do + it_behaves_like 'new label' + end + + context 'when user has no permissions to create labels' do + let_it_be(:user) { create(:user) } + + it_behaves_like 'new label' + end + end + end +end diff --git a/spec/support/shared_examples/services/merge_request_shared_examples.rb b/spec/support/shared_examples/services/merge_request_shared_examples.rb index a7032640217..2bd06ac3e9c 100644 --- a/spec/support/shared_examples/services/merge_request_shared_examples.rb +++ b/spec/support/shared_examples/services/merge_request_shared_examples.rb @@ -13,11 +13,10 @@ RSpec.shared_examples 'reviewer_ids filter' do end context 'with reviewer_ids' do - let(:reviewer_ids_param) { { reviewer_ids: [reviewer1.id, reviewer2.id, reviewer3.id] } } + let(:reviewer_ids_param) { { reviewer_ids: [reviewer1.id, reviewer2.id] } } let(:reviewer1) { create(:user) } let(:reviewer2) { create(:user) } - let(:reviewer3) { create(:user) } context 'when the current user can admin the merge_request' do context 'when merge_request_reviewer feature is enabled' do @@ -25,14 +24,13 @@ RSpec.shared_examples 'reviewer_ids filter' do stub_feature_flags(merge_request_reviewer: true) end - context 'with reviewers who can read the merge_request' do + context 'with a reviewer who can read the merge_request' do before do project.add_developer(reviewer1) - project.add_developer(reviewer2) end it 'contains reviewers who can read the merge_request' do - expect(execute.reviewers).to contain_exactly(reviewer1, reviewer2) + expect(execute.reviewers).to contain_exactly(reviewer1) end end end diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb index 7fd59c3d963..65f4b3b5513 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -170,6 +170,8 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false| let_it_be(:package5) { create(:pypi_package, project: project) } let_it_be(:package6) { create(:composer_package, project: project) } let_it_be(:package7) { create(:generic_package, project: project) } + let_it_be(:package8) { create(:golang_package, project: project) } + let_it_be(:package9) { create(:debian_package, project: project) } Packages::Package.package_types.keys.each do |package_type| context "for package type #{package_type}" do diff --git a/spec/support/shared_examples/services/projects/urls_with_escaped_elements_shared_example.rb b/spec/support/shared_examples/services/projects/urls_with_escaped_elements_shared_example.rb new file mode 100644 index 00000000000..df8b1f91629 --- /dev/null +++ b/spec/support/shared_examples/services/projects/urls_with_escaped_elements_shared_example.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Shared examples that test requests against URLs with escaped elements +# +RSpec.shared_examples "URLs containing escaped elements return expected status" do + using RSpec::Parameterized::TableSyntax + + where(:url, :result_status) do + "https://user:0a%23@test.example.com/project.git" | :success + "https://git.example.com:1%2F%2F@source.developers.google.com/project.git" | :success + CGI.escape("git://localhost:1234/some-path?some-query=some-val\#@example.com/") | :error + CGI.escape(CGI.escape("https://user:0a%23@test.example.com/project.git")) | :error + end + + with_them do + it "returns expected status" do + expect(result[:status]).to eq(result_status) + end + end +end diff --git a/spec/support/shared_examples/validators/ip_address_validator_shared_examples.rb b/spec/support/shared_examples/validators/ip_address_validator_shared_examples.rb new file mode 100644 index 00000000000..5680d4f772c --- /dev/null +++ b/spec/support/shared_examples/validators/ip_address_validator_shared_examples.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'validates IP address' do + subject { object } + + it { is_expected.to allow_value('192.168.17.43').for(attribute.to_sym) } + it { is_expected.to allow_value('2001:0db8:85a3:0000:0000:8a2e:0370:7334').for(attribute.to_sym) } + + it { is_expected.not_to allow_value('invalid IP').for(attribute.to_sym) } +end |