diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-20 18:42:06 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-08-20 18:42:06 +0000 |
commit | 6e4e1050d9dba2b7b2523fdd1768823ab85feef4 (patch) | |
tree | 78be5963ec075d80116a932011d695dd33910b4e /spec/support/shared_examples | |
parent | 1ce776de4ae122aba3f349c02c17cebeaa8ecf07 (diff) | |
download | gitlab-ce-6e4e1050d9dba2b7b2523fdd1768823ab85feef4.tar.gz |
Add latest changes from gitlab-org/gitlab@13-3-stable-ee
Diffstat (limited to 'spec/support/shared_examples')
54 files changed, 2530 insertions, 617 deletions
diff --git a/spec/support/shared_examples/alert_notification_service_shared_examples.rb b/spec/support/shared_examples/alert_notification_service_shared_examples.rb new file mode 100644 index 00000000000..1568e4357a1 --- /dev/null +++ b/spec/support/shared_examples/alert_notification_service_shared_examples.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'Alert Notification Service sends notification email' do + let(:notification_service) { spy } + + it 'sends a notification for firing alerts only' do + expect(NotificationService) + .to receive(:new) + .and_return(notification_service) + + expect(notification_service) + .to receive_message_chain(:async, :prometheus_alerts_fired) + + expect(subject).to be_success + end +end + +RSpec.shared_examples 'Alert Notification Service sends no notifications' do |http_status:| + let(:notification_service) { spy } + let(:create_events_service) { spy } + + it 'does not notify' do + expect(notification_service).not_to receive(:async) + expect(create_events_service).not_to receive(:execute) + + expect(subject).to be_error + expect(subject.http_status).to eq(http_status) + end +end diff --git a/spec/support/shared_examples/controllers/binary_blob_shared_examples.rb b/spec/support/shared_examples/controllers/binary_blob_shared_examples.rb index c1ec515f1fe..acce7642cfe 100644 --- a/spec/support/shared_examples/controllers/binary_blob_shared_examples.rb +++ b/spec/support/shared_examples/controllers/binary_blob_shared_examples.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true RSpec.shared_examples 'editing snippet checks blob is binary' do + let(:snippets_binary_blob_value) { true } + before do sign_in(user) @@ -8,6 +10,8 @@ RSpec.shared_examples 'editing snippet checks blob is binary' do allow(blob).to receive(:binary?).and_return(binary) end + stub_feature_flags(snippets_binary_blob: snippets_binary_blob_value) + subject end @@ -23,13 +27,24 @@ RSpec.shared_examples 'editing snippet checks blob is binary' do context 'when blob is binary' do let(:binary) { true } - it 'redirects away' do - expect(response).to redirect_to(gitlab_snippet_path(snippet)) + it 'responds with status 200' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:edit) + end + + context 'when feature flag :snippets_binary_blob is disabled' do + let(:snippets_binary_blob_value) { false } + + it 'redirects away' do + expect(response).to redirect_to(gitlab_snippet_path(snippet)) + end end end end RSpec.shared_examples 'updating snippet checks blob is binary' do + let(:snippets_binary_blob_value) { true } + before do sign_in(user) @@ -37,6 +52,8 @@ RSpec.shared_examples 'updating snippet checks blob is binary' do allow(blob).to receive(:binary?).and_return(binary) end + stub_feature_flags(snippets_binary_blob: snippets_binary_blob_value) + subject end @@ -52,9 +69,18 @@ RSpec.shared_examples 'updating snippet checks blob is binary' do context 'when blob is binary' do let(:binary) { true } - it 'redirects away without updating' do + it 'updates successfully' do + expect(snippet.reload.title).to eq title expect(response).to redirect_to(gitlab_snippet_path(snippet)) - expect(snippet.reload.title).not_to eq title + end + + context 'when feature flag :snippets_binary_blob is disabled' do + let(:snippets_binary_blob_value) { false } + + it 'redirects away without updating' do + expect(response).to redirect_to(gitlab_snippet_path(snippet)) + expect(snippet.reload.title).not_to eq title + end end end end diff --git a/spec/support/shared_examples/controllers/concerns/graceful_timeout_handling_shared_examples.rb b/spec/support/shared_examples/controllers/concerns/graceful_timeout_handling_shared_examples.rb new file mode 100644 index 00000000000..ea002776eeb --- /dev/null +++ b/spec/support/shared_examples/controllers/concerns/graceful_timeout_handling_shared_examples.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.shared_examples GracefulTimeoutHandling do + it 'includes GracefulTimeoutHandling' do + expect(controller).to be_a(GracefulTimeoutHandling) + 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 a01fa49d701..8bc91f72b8c 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 @@ -72,7 +72,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') group = create(:group) group.add_owner(user) - stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo]) + stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo], each_page: [OpenStruct.new(objects: [repo, org_repo])].to_enum) get :status, format: :json @@ -85,7 +85,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do it "does not show already added project" do project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'asd/vim') - stub_client(repos: [repo], orgs: []) + stub_client(repos: [repo], orgs: [], each_page: [OpenStruct.new(objects: [repo])].to_enum) get :status, format: :json @@ -94,7 +94,8 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do end it "touches the etag cache store" do - expect(stub_client(repos: [], orgs: [])).to receive(:repos) + stub_client(repos: [], orgs: [], each_page: []) + expect_next_instance_of(Gitlab::EtagCaching::Store) do |store| expect(store).to receive(:touch) { "realtime_changes_import_#{provider}_path" } end @@ -102,17 +103,11 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do get :status, format: :json end - it "requests provider repos list" do - expect(stub_client(repos: [], orgs: [])).to receive(:repos) - - get :status - - expect(response).to have_gitlab_http_status(:ok) - end - it "handles an invalid access token" do - allow_any_instance_of(Gitlab::LegacyGithubImport::Client) - .to receive(:repos).and_raise(Octokit::Unauthorized) + client = stub_client(repos: [], orgs: [], each_page: []) + + allow(client).to receive(:repos).and_raise(Octokit::Unauthorized) + allow(client).to receive(:each_page).and_raise(Octokit::Unauthorized) get :status @@ -122,7 +117,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do end it "does not produce N+1 database queries" do - stub_client(repos: [repo], orgs: []) + stub_client(repos: [repo], orgs: [], each_page: [].to_enum) group_a = create(:group) group_a.add_owner(user) create(:project, :import_started, import_type: provider, namespace: user.namespace) @@ -144,10 +139,12 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do let(:repo_2) { OpenStruct.new(login: 'emacs', full_name: 'asd/emacs', name: 'emacs', owner: { login: 'owner' }) } let(:project) { create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') } let(:group) { create(:group) } + let(:repos) { [repo, repo_2, org_repo] } before do group.add_owner(user) - stub_client(repos: [repo, repo_2, org_repo], orgs: [org], org_repos: [org_repo]) + client = stub_client(repos: repos, orgs: [org], org_repos: [org_repo]) + allow(client).to receive(:each_page).and_return([OpenStruct.new(objects: repos)].to_enum) end it 'filters list of repositories by name' do @@ -187,14 +184,14 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do end before do - stub_client(user: provider_user, repo: provider_repo) + stub_client(user: provider_user, repo: provider_repo, repository: provider_repo) assign_session_token(provider) end it 'returns 200 response when the project is imported successfully' do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, format: :json @@ -208,7 +205,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, format: :json @@ -219,7 +216,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it "touches the etag cache store" do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) expect_next_instance_of(Gitlab::EtagCaching::Store) do |store| expect(store).to receive(:touch) { "realtime_changes_import_#{provider}_path" } end @@ -232,7 +229,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it "takes the current user's namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, format: :json end @@ -244,7 +241,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it "takes the current user's namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, format: :json end @@ -271,7 +268,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it "takes the existing namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, format: :json end @@ -283,7 +280,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, format: :json end @@ -302,7 +299,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it "takes the new namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, params: { target_namespace: provider_repo.name }, format: :json end @@ -323,7 +320,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it "takes the current user's namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, format: :json end @@ -341,7 +338,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'takes the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, params: { target_namespace: test_namespace.name, new_name: test_name }, format: :json end @@ -349,7 +346,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'takes the selected name and default namespace' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, params: { new_name: test_name }, format: :json end @@ -368,7 +365,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'takes the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, nested_namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, params: { target_namespace: nested_namespace.full_path, new_name: test_name }, format: :json end @@ -380,7 +377,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'takes the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo/bar', new_name: test_name }, format: :json end @@ -388,7 +385,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'creates the namespaces' do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) expect { post :create, params: { target_namespace: 'foo/bar', new_name: test_name }, format: :json } .to change { Namespace.count }.by(2) @@ -397,7 +394,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'new namespace has the right parent' do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo/bar', new_name: test_name }, format: :json @@ -416,7 +413,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'takes the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :json end @@ -424,7 +421,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'creates the namespaces' do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) expect { post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :json } .to change { Namespace.count }.by(2) @@ -432,11 +429,11 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'does not create a new namespace under the user namespace' do expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: project)) expect { post :create, params: { target_namespace: "#{user.namespace_path}/test_group", new_name: test_name }, format: :js } - .not_to change { Namespace.count } + .not_to change { Namespace.count } end end @@ -446,19 +443,19 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'does not take the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :js end it 'does not create the namespaces' do allow(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: project)) + .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) + .and_return(double(execute: project)) expect { post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :js } - .not_to change { Namespace.count } + .not_to change { Namespace.count } end end @@ -471,8 +468,8 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do user.update!(can_create_group: false) expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, group, user, access_params, type: provider) - .and_return(double(execute: project)) + .to receive(:new).with(provider_repo, test_name, group, user, access_params, type: provider) + .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo', new_name: test_name }, format: :js end diff --git a/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb b/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb index 94cd6971f7c..19b1cee44ee 100644 --- a/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb +++ b/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb @@ -9,6 +9,7 @@ RSpec.shared_examples_for 'metrics dashboard prometheus api proxy' do id: proxyable.id.to_s } end + let(:expected_params) do ActionController::Parameters.new( prometheus_proxy_params( diff --git a/spec/support/shared_examples/controllers/variables_shared_examples.rb b/spec/support/shared_examples/controllers/variables_shared_examples.rb index 9ff0bc3d217..34632993cf0 100644 --- a/spec/support/shared_examples/controllers/variables_shared_examples.rb +++ b/spec/support/shared_examples/controllers/variables_shared_examples.rb @@ -21,6 +21,7 @@ RSpec.shared_examples 'PATCH #update updates variables' do secret_value: variable.value, protected: variable.protected?.to_s } end + let(:new_variable_attributes) do { key: 'new_key', secret_value: 'dummy_value', diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb index 4df3139d56e..c89ee0d25ae 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -61,6 +61,14 @@ RSpec.shared_examples 'wiki controller actions' do expect(assigns(:sidebar_wiki_entries)).to be_nil expect(assigns(:sidebar_limited)).to be_nil end + + context 'when the request is of non-html format' do + it 'returns a 404 error' do + get :pages, params: routing_params.merge(format: 'json') + + expect(response).to have_gitlab_http_status(:not_found) + end + end end describe 'GET #history' do @@ -153,6 +161,14 @@ 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 + + expect(response).to have_gitlab_http_status(:ok) + end.to change { Gitlab::UsageDataCounters::WikiPageCounter.read(:view) }.by(1) + end + context 'when page content encoding is invalid' do it 'sets flash error' do allow(controller).to receive(:valid_encoding?).and_return(false) @@ -339,6 +355,44 @@ RSpec.shared_examples 'wiki controller actions' do end end + describe 'POST #create' do + let(:new_title) { 'New title' } + let(:new_content) { 'New content' } + + subject do + post(:create, + params: routing_params.merge( + wiki: { title: new_title, content: new_content } + )) + end + + context 'when page is valid' do + it 'creates the page' do + expect do + subject + end.to change { wiki.list_pages.size }.by 1 + + wiki_page = wiki.find_page(new_title) + + expect(wiki_page.title).to eq new_title + expect(wiki_page.content).to eq new_content + end + end + + context 'when page is not valid' do + let(:new_title) { '' } + + it 'renders the edit state' do + expect do + subject + end.not_to change { wiki.list_pages.size } + + expect(response).to render_template('shared/wikis/edit') + expect(flash[:alert]).to eq('Could not create wiki page') + end + end + end + def redirect_to_wiki(wiki, page) redirect_to(controller.wiki_page_path(wiki, page)) end diff --git a/spec/support/shared_examples/create_alert_issue_shared_examples.rb b/spec/support/shared_examples/create_alert_issue_shared_examples.rb deleted file mode 100644 index 9f4e1c4335a..00000000000 --- a/spec/support/shared_examples/create_alert_issue_shared_examples.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'create alert issue sets issue labels' do - let(:title) { IncidentManagement::CreateIncidentLabelService::LABEL_PROPERTIES[:title] } - let!(:label) { create(:label, project: project, title: title) } - let(:label_service) { instance_double(IncidentManagement::CreateIncidentLabelService, execute: label_service_response) } - - before do - allow(IncidentManagement::CreateIncidentLabelService).to receive(:new).with(project, user).and_return(label_service) - end - - context 'when create incident label responds with success' do - let(:label_service_response) { ServiceResponse.success(payload: { label: label }) } - - it 'adds label to issue' do - expect(issue.labels).to eq([label]) - end - end - - context 'when create incident label responds with error' do - let(:label_service_response) { ServiceResponse.error(payload: { label: label }, message: 'label error') } - - it 'creates an issue without labels' do - expect(issue.labels).to be_empty - end - end -end diff --git a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb index 00ce690d2e3..ffe4fb83283 100644 --- a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb +++ b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb @@ -8,17 +8,18 @@ RSpec.shared_examples 'Maintainer manages access requests' do entity.request_access(user) entity.respond_to?(:add_owner) ? entity.add_owner(maintainer) : entity.add_maintainer(maintainer) sign_in(maintainer) - end - - it 'maintainer can see access requests' do visit members_page_path + if has_tabs + click_on 'Access requests' + end + end + + it 'maintainer can see access requests', :js do expect_visible_access_request(entity, user) end it 'maintainer can grant access', :js do - visit members_page_path - expect_visible_access_request(entity, user) click_on 'Grant access' @@ -31,8 +32,6 @@ RSpec.shared_examples 'Maintainer manages access requests' do end it 'maintainer can deny access', :js do - visit members_page_path - expect_visible_access_request(entity, user) # Open modal @@ -47,7 +46,13 @@ RSpec.shared_examples 'Maintainer manages access requests' do end def expect_visible_access_request(entity, user) - expect(page).to have_content "Users requesting access to #{entity.name} 1" + if has_tabs + expect(page).to have_content "Access requests 1" + expect(page).to have_content "Users requesting access to #{entity.name}" + else + expect(page).to have_content "Users requesting access to #{entity.name} 1" + end + expect(page).to have_content user.name end diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb new file mode 100644 index 00000000000..6debbf81fc0 --- /dev/null +++ b/spec/support/shared_examples/features/packages_shared_examples.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'packages list' do |check_project_name: false| + it 'shows a list of packages' do + wait_for_requests + + packages.each_with_index do |pkg, index| + package_row = package_table_row(index) + + expect(package_row).to have_content(pkg.name) + expect(package_row).to have_content(pkg.version) + expect(package_row).to have_content(pkg.project.name) if check_project_name + end + end + + def package_table_row(index) + page.all("#{packages_table_selector} > [data-qa-selector=\"packages-row\"]")[index].text + end +end + +RSpec.shared_examples 'package details link' do |property| + let(:package) { packages.first } + + before do + stub_feature_flags(packages_details_one_column: false) + end + + it 'navigates to the correct url' do + page.within(packages_table_selector) do + click_link package.name + end + + expect(page).to have_current_path(project_package_path(package.project, package)) + + page.within('.detail-page-header') do + expect(page).to have_content(package.name) + end + + page.within('[data-qa-selector="package_information_content"]') do + expect(page).to have_content('Installation') + expect(page).to have_content('Registry setup') + end + end +end + +RSpec.shared_examples 'when there are no packages' do + it 'displays the empty message' do + expect(page).to have_content('There are no packages yet') + end +end + +RSpec.shared_examples 'correctly sorted packages list' do |order_by, ascending: false| + context "ordered by #{order_by} and ascending #{ascending}" do + before do + click_sort_option(order_by, ascending) + end + + it_behaves_like 'packages list' + end +end + +RSpec.shared_examples 'shared package sorting' do + it_behaves_like 'correctly sorted packages list', 'Type' do + let(:packages) { [package_two, package_one] } + end + + it_behaves_like 'correctly sorted packages list', 'Type', ascending: true do + let(:packages) { [package_one, package_two] } + end + + it_behaves_like 'correctly sorted packages list', 'Name' do + let(:packages) { [package_two, package_one] } + end + + it_behaves_like 'correctly sorted packages list', 'Name', ascending: true do + let(:packages) { [package_one, package_two] } + end + + it_behaves_like 'correctly sorted packages list', 'Version' do + let(:packages) { [package_one, package_two] } + end + + it_behaves_like 'correctly sorted packages list', 'Version', ascending: true do + let(:packages) { [package_two, package_one] } + end + + it_behaves_like 'correctly sorted packages list', 'Created' do + let(:packages) { [package_two, package_one] } + end + + it_behaves_like 'correctly sorted packages list', 'Created', ascending: true do + let(:packages) { [package_one, package_two] } + end +end + +def packages_table_selector + '[data-qa-selector="packages-table"]' +end + +def click_sort_option(option, ascending) + page.within('.gl-sorting') do + # Reset the sort direction + click_button 'Sort direction' if page.has_selector?('svg[aria-label="Sorting Direction: Ascending"]', wait: 0) + + find('button.dropdown-menu-toggle').click + + page.within('.dropdown-menu') do + click_button option + end + + click_button 'Sort direction' if ascending + end +end diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb index 65db082505a..a46382bc292 100644 --- a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb +++ b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb @@ -22,7 +22,7 @@ RSpec.shared_examples "protected branches > access control > CE" do end end - click_on "Protect" + click_on_protect expect(ProtectedBranch.count).to eq(1) expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id]) @@ -45,7 +45,7 @@ RSpec.shared_examples "protected branches > access control > CE" do find(:link, 'No one').click end - click_on "Protect" + click_on_protect expect(ProtectedBranch.count).to eq(1) @@ -85,7 +85,7 @@ RSpec.shared_examples "protected branches > access control > CE" do find(:link, 'No one').click end - click_on "Protect" + click_on_protect expect(ProtectedBranch.count).to eq(1) expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id]) @@ -108,7 +108,7 @@ RSpec.shared_examples "protected branches > access control > CE" do find(:link, 'No one').click end - click_on "Protect" + click_on_protect expect(ProtectedBranch.count).to eq(1) diff --git a/spec/support/shared_examples/features/rss_shared_examples.rb b/spec/support/shared_examples/features/rss_shared_examples.rb index 42df88ec08e..1b0d3f9605a 100644 --- a/spec/support/shared_examples/features/rss_shared_examples.rb +++ b/spec/support/shared_examples/features/rss_shared_examples.rb @@ -9,8 +9,7 @@ end RSpec.shared_examples "it has an RSS button with current_user's feed token" do it "shows the RSS button with current_user's feed token" do expect(page) - .to have_css("a:has(.fa-rss)[href*='feed_token=#{user.feed_token}']") - .or have_css("a.js-rss-button[href*='feed_token=#{user.feed_token}']") + .to have_css("a:has(.qa-rss-icon)[href*='feed_token=#{user.feed_token}']") end end @@ -23,7 +22,6 @@ end RSpec.shared_examples "it has an RSS button without a feed token" do it "shows the RSS button without a feed token" do expect(page) - .to have_css("a:has(.fa-rss):not([href*='feed_token'])") - .or have_css("a.js-rss-button:not([href*='feed_token'])") + .to have_css("a:has(.qa-rss-icon):not([href*='feed_token'])") 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 1c8a9714bdf..8d68b1e4c0a 100644 --- a/spec/support/shared_examples/features/snippets_shared_examples.rb +++ b/spec/support/shared_examples/features/snippets_shared_examples.rb @@ -50,3 +50,225 @@ RSpec.shared_examples 'tabs with counts' do expect(tab.find('.badge').text).to eq(counts[:public]) end end + +RSpec.shared_examples 'does not show New Snippet button' do + let(:user) { create(:user, :external) } + + specify do + sign_in(user) + + subject + + wait_for_requests + + expect(page).not_to have_link('New snippet') + end +end + +RSpec.shared_examples 'show and render proper snippet blob' do + before do + allow_any_instance_of(Snippet).to receive(:blobs).and_return([snippet.repository.blob_at('master', file_path)]) + end + + context 'Ruby file' do + let(:file_path) { 'files/ruby/popen.rb' } + + it 'displays the blob' do + subject + + aggregate_failures do + # shows highlighted Ruby code + expect(page).to have_content("require 'fileutils'") + + # does not show a viewer switcher + 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)') + + # shows a raw button + expect(page).to have_link('Open raw') + + # shows a download button + expect(page).to have_link('Download') + end + end + end + + context 'Markdown file' do + let(:file_path) { 'files/markdown/ruby-style-guide.md' } + + context 'visiting directly' do + before do + subject + end + + 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 + expect(page).to have_link("PEP-8") + + # shows a viewer switcher + 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') + + # shows a raw button + expect(page).to have_link('Open raw') + + # shows a download button + expect(page).to have_link('Download') + end + end + + context 'switching to the simple viewer' do + before do + find('.js-blob-viewer-switch-btn[data-viewer=simple]').click + + wait_for_requests + end + + it 'displays the blob using the simple viewer' 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)') + end + end + + context 'switching to the rich viewer again' do + before do + find('.js-blob-viewer-switch-btn[data-viewer=rich]').click + + wait_for_requests + end + + 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)') + end + end + end + end + end + + context 'visiting with a line number anchor' do + let(:anchor) { 'L1' } + + it 'displays the blob using the simple viewer' do + subject + + 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') + + # 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)') + end + end + end + end +end + +RSpec.shared_examples 'personal snippet with references' do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + let_it_be(:project_snippet) { create(:project_snippet, :repository, project: project)} + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:commit) { project.commit } + + let(:mr_reference) { merge_request.to_reference(full: true) } + let(:issue_reference) { issue.to_reference(full: true) } + let(:snippet_reference) { project_snippet.to_reference(full: true) } + let(:commit_reference) { commit.reference_link_text(full: true) } + + RSpec.shared_examples 'handles resource links' do + context 'with access to the resource' do + before do + project.add_developer(user) + end + + it 'converts the reference to a link' do + subject + + page.within(container) do + aggregate_failures do + expect(page).to have_link(mr_reference) + expect(page).to have_link(issue_reference) + expect(page).to have_link(snippet_reference) + expect(page).to have_link(commit_reference) + end + end + end + end + + context 'without access to the resource' do + it 'does not convert the reference to a link' do + subject + + page.within(container) do + expect(page).not_to have_link(mr_reference) + expect(page).not_to have_link(issue_reference) + expect(page).not_to have_link(snippet_reference) + expect(page).not_to have_link(commit_reference) + end + end + end + end + + context 'when using references to resources' do + let(:references) do + <<~REFERENCES + MR: #{mr_reference} + + Commit: #{commit_reference} + + Issue: #{issue_reference} + + ProjectSnippet: #{snippet_reference} + REFERENCES + end + + it_behaves_like 'handles resource links' + end + + context 'when using links to resources' do + let(:args) { { host: Gitlab.config.gitlab.url, port: nil } } + let(:references) do + <<~REFERENCES + MR: #{merge_request_url(merge_request, args)} + + Commit: #{project_commit_url(project, commit, args)} + + Issue: #{issue_url(issue, args)} + + ProjectSnippet: #{project_snippet_url(project, project_snippet, args)} + REFERENCES + end + + it_behaves_like 'handles resource links' + end +end diff --git a/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb b/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb index c802038c9da..a2c34cdd4a1 100644 --- a/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb +++ b/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb @@ -9,13 +9,28 @@ RSpec.shared_examples 'snippet visibility' do let_it_be(:non_member) { create(:user) } let_it_be(:project, reload: true) do - create(:project).tap do |project| + create(:project, :public).tap do |project| project.add_developer(author) project.add_developer(member) end end + let(:snippets) do + { + private: private_snippet, + public: public_snippet, + internal: internal_snippet + } + end + + let(:user) { users[user_type] } + let(:snippet) { snippets[snippet_visibility] } + context "For project snippets" do + let_it_be(:private_snippet) { create(:project_snippet, :private, project: project, author: author) } + let_it_be(:public_snippet) { create(:project_snippet, :public, project: project, author: author) } + let_it_be(:internal_snippet) { create(:project_snippet, :internal, project: project, author: author) } + let!(:users) do { unauthenticated: nil, @@ -26,214 +41,212 @@ RSpec.shared_examples 'snippet visibility' do } end - where(:project_type, :feature_visibility, :user_type, :snippet_type, :outcome) do + where(:project_visibility, :feature_visibility, :user_type, :snippet_visibility, :outcome) do [ # Public projects - [:public, ProjectFeature::ENABLED, :unauthenticated, Snippet::PUBLIC, true], - [:public, ProjectFeature::ENABLED, :unauthenticated, Snippet::INTERNAL, false], - [:public, ProjectFeature::ENABLED, :unauthenticated, Snippet::PRIVATE, false], + [:public, :enabled, :unauthenticated, :public, true], + [:public, :enabled, :unauthenticated, :internal, false], + [:public, :enabled, :unauthenticated, :private, false], - [:public, ProjectFeature::ENABLED, :external, Snippet::PUBLIC, true], - [:public, ProjectFeature::ENABLED, :external, Snippet::INTERNAL, false], - [:public, ProjectFeature::ENABLED, :external, Snippet::PRIVATE, false], + [:public, :enabled, :external, :public, true], + [:public, :enabled, :external, :internal, false], + [:public, :enabled, :external, :private, false], - [:public, ProjectFeature::ENABLED, :non_member, Snippet::PUBLIC, true], - [:public, ProjectFeature::ENABLED, :non_member, Snippet::INTERNAL, true], - [:public, ProjectFeature::ENABLED, :non_member, Snippet::PRIVATE, false], + [:public, :enabled, :non_member, :public, true], + [:public, :enabled, :non_member, :internal, true], + [:public, :enabled, :non_member, :private, false], - [:public, ProjectFeature::ENABLED, :member, Snippet::PUBLIC, true], - [:public, ProjectFeature::ENABLED, :member, Snippet::INTERNAL, true], - [:public, ProjectFeature::ENABLED, :member, Snippet::PRIVATE, true], + [:public, :enabled, :member, :public, true], + [:public, :enabled, :member, :internal, true], + [:public, :enabled, :member, :private, true], - [:public, ProjectFeature::ENABLED, :author, Snippet::PUBLIC, true], - [:public, ProjectFeature::ENABLED, :author, Snippet::INTERNAL, true], - [:public, ProjectFeature::ENABLED, :author, Snippet::PRIVATE, true], + [:public, :enabled, :author, :public, true], + [:public, :enabled, :author, :internal, true], + [:public, :enabled, :author, :private, true], - [:public, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PUBLIC, false], - [:public, ProjectFeature::PRIVATE, :unauthenticated, Snippet::INTERNAL, false], - [:public, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PRIVATE, false], + [:public, :private, :unauthenticated, :public, false], + [:public, :private, :unauthenticated, :internal, false], + [:public, :private, :unauthenticated, :private, false], - [:public, ProjectFeature::PRIVATE, :external, Snippet::PUBLIC, false], - [:public, ProjectFeature::PRIVATE, :external, Snippet::INTERNAL, false], - [:public, ProjectFeature::PRIVATE, :external, Snippet::PRIVATE, false], + [:public, :private, :external, :public, false], + [:public, :private, :external, :internal, false], + [:public, :private, :external, :private, false], - [:public, ProjectFeature::PRIVATE, :non_member, Snippet::PUBLIC, false], - [:public, ProjectFeature::PRIVATE, :non_member, Snippet::INTERNAL, false], - [:public, ProjectFeature::PRIVATE, :non_member, Snippet::PRIVATE, false], + [:public, :private, :non_member, :public, false], + [:public, :private, :non_member, :internal, false], + [:public, :private, :non_member, :private, false], - [:public, ProjectFeature::PRIVATE, :member, Snippet::PUBLIC, true], - [:public, ProjectFeature::PRIVATE, :member, Snippet::INTERNAL, true], - [:public, ProjectFeature::PRIVATE, :member, Snippet::PRIVATE, true], + [:public, :private, :member, :public, true], + [:public, :private, :member, :internal, true], + [:public, :private, :member, :private, true], - [:public, ProjectFeature::PRIVATE, :author, Snippet::PUBLIC, true], - [:public, ProjectFeature::PRIVATE, :author, Snippet::INTERNAL, true], - [:public, ProjectFeature::PRIVATE, :author, Snippet::PRIVATE, true], + [:public, :private, :author, :public, true], + [:public, :private, :author, :internal, true], + [:public, :private, :author, :private, true], - [:public, ProjectFeature::DISABLED, :unauthenticated, Snippet::PUBLIC, false], - [:public, ProjectFeature::DISABLED, :unauthenticated, Snippet::INTERNAL, false], - [:public, ProjectFeature::DISABLED, :unauthenticated, Snippet::PRIVATE, false], + [:public, :disabled, :unauthenticated, :public, false], + [:public, :disabled, :unauthenticated, :internal, false], + [:public, :disabled, :unauthenticated, :private, false], - [:public, ProjectFeature::DISABLED, :external, Snippet::PUBLIC, false], - [:public, ProjectFeature::DISABLED, :external, Snippet::INTERNAL, false], - [:public, ProjectFeature::DISABLED, :external, Snippet::PRIVATE, false], + [:public, :disabled, :external, :public, false], + [:public, :disabled, :external, :internal, false], + [:public, :disabled, :external, :private, false], - [:public, ProjectFeature::DISABLED, :non_member, Snippet::PUBLIC, false], - [:public, ProjectFeature::DISABLED, :non_member, Snippet::INTERNAL, false], - [:public, ProjectFeature::DISABLED, :non_member, Snippet::PRIVATE, false], + [:public, :disabled, :non_member, :public, false], + [:public, :disabled, :non_member, :internal, false], + [:public, :disabled, :non_member, :private, false], - [:public, ProjectFeature::DISABLED, :member, Snippet::PUBLIC, false], - [:public, ProjectFeature::DISABLED, :member, Snippet::INTERNAL, false], - [:public, ProjectFeature::DISABLED, :member, Snippet::PRIVATE, false], + [:public, :disabled, :member, :public, false], + [:public, :disabled, :member, :internal, false], + [:public, :disabled, :member, :private, false], - [:public, ProjectFeature::DISABLED, :author, Snippet::PUBLIC, false], - [:public, ProjectFeature::DISABLED, :author, Snippet::INTERNAL, false], - [:public, ProjectFeature::DISABLED, :author, Snippet::PRIVATE, false], + [:public, :disabled, :author, :public, false], + [:public, :disabled, :author, :internal, false], + [:public, :disabled, :author, :private, false], # Internal projects - [:internal, ProjectFeature::ENABLED, :unauthenticated, Snippet::PUBLIC, false], - [:internal, ProjectFeature::ENABLED, :unauthenticated, Snippet::INTERNAL, false], - [:internal, ProjectFeature::ENABLED, :unauthenticated, Snippet::PRIVATE, false], + [:internal, :enabled, :unauthenticated, :public, false], + [:internal, :enabled, :unauthenticated, :internal, false], + [:internal, :enabled, :unauthenticated, :private, false], - [:internal, ProjectFeature::ENABLED, :external, Snippet::PUBLIC, false], - [:internal, ProjectFeature::ENABLED, :external, Snippet::INTERNAL, false], - [:internal, ProjectFeature::ENABLED, :external, Snippet::PRIVATE, false], + [:internal, :enabled, :external, :public, false], + [:internal, :enabled, :external, :internal, false], + [:internal, :enabled, :external, :private, false], - [:internal, ProjectFeature::ENABLED, :non_member, Snippet::PUBLIC, true], - [:internal, ProjectFeature::ENABLED, :non_member, Snippet::INTERNAL, true], - [:internal, ProjectFeature::ENABLED, :non_member, Snippet::PRIVATE, false], + [:internal, :enabled, :non_member, :public, true], + [:internal, :enabled, :non_member, :internal, true], + [:internal, :enabled, :non_member, :private, false], - [:internal, ProjectFeature::ENABLED, :member, Snippet::PUBLIC, true], - [:internal, ProjectFeature::ENABLED, :member, Snippet::INTERNAL, true], - [:internal, ProjectFeature::ENABLED, :member, Snippet::PRIVATE, true], + [:internal, :enabled, :member, :public, true], + [:internal, :enabled, :member, :internal, true], + [:internal, :enabled, :member, :private, true], - [:internal, ProjectFeature::ENABLED, :author, Snippet::PUBLIC, true], - [:internal, ProjectFeature::ENABLED, :author, Snippet::INTERNAL, true], - [:internal, ProjectFeature::ENABLED, :author, Snippet::PRIVATE, true], + [:internal, :enabled, :author, :public, true], + [:internal, :enabled, :author, :internal, true], + [:internal, :enabled, :author, :private, true], - [:internal, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PUBLIC, false], - [:internal, ProjectFeature::PRIVATE, :unauthenticated, Snippet::INTERNAL, false], - [:internal, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PRIVATE, false], + [:internal, :private, :unauthenticated, :public, false], + [:internal, :private, :unauthenticated, :internal, false], + [:internal, :private, :unauthenticated, :private, false], - [:internal, ProjectFeature::PRIVATE, :external, Snippet::PUBLIC, false], - [:internal, ProjectFeature::PRIVATE, :external, Snippet::INTERNAL, false], - [:internal, ProjectFeature::PRIVATE, :external, Snippet::PRIVATE, false], + [:internal, :private, :external, :public, false], + [:internal, :private, :external, :internal, false], + [:internal, :private, :external, :private, false], - [:internal, ProjectFeature::PRIVATE, :non_member, Snippet::PUBLIC, false], - [:internal, ProjectFeature::PRIVATE, :non_member, Snippet::INTERNAL, false], - [:internal, ProjectFeature::PRIVATE, :non_member, Snippet::PRIVATE, false], + [:internal, :private, :non_member, :public, false], + [:internal, :private, :non_member, :internal, false], + [:internal, :private, :non_member, :private, false], - [:internal, ProjectFeature::PRIVATE, :member, Snippet::PUBLIC, true], - [:internal, ProjectFeature::PRIVATE, :member, Snippet::INTERNAL, true], - [:internal, ProjectFeature::PRIVATE, :member, Snippet::PRIVATE, true], + [:internal, :private, :member, :public, true], + [:internal, :private, :member, :internal, true], + [:internal, :private, :member, :private, true], - [:internal, ProjectFeature::PRIVATE, :author, Snippet::PUBLIC, true], - [:internal, ProjectFeature::PRIVATE, :author, Snippet::INTERNAL, true], - [:internal, ProjectFeature::PRIVATE, :author, Snippet::PRIVATE, true], + [:internal, :private, :author, :public, true], + [:internal, :private, :author, :internal, true], + [:internal, :private, :author, :private, true], - [:internal, ProjectFeature::DISABLED, :unauthenticated, Snippet::PUBLIC, false], - [:internal, ProjectFeature::DISABLED, :unauthenticated, Snippet::INTERNAL, false], - [:internal, ProjectFeature::DISABLED, :unauthenticated, Snippet::PRIVATE, false], + [:internal, :disabled, :unauthenticated, :public, false], + [:internal, :disabled, :unauthenticated, :internal, false], + [:internal, :disabled, :unauthenticated, :private, false], - [:internal, ProjectFeature::DISABLED, :external, Snippet::PUBLIC, false], - [:internal, ProjectFeature::DISABLED, :external, Snippet::INTERNAL, false], - [:internal, ProjectFeature::DISABLED, :external, Snippet::PRIVATE, false], + [:internal, :disabled, :external, :public, false], + [:internal, :disabled, :external, :internal, false], + [:internal, :disabled, :external, :private, false], - [:internal, ProjectFeature::DISABLED, :non_member, Snippet::PUBLIC, false], - [:internal, ProjectFeature::DISABLED, :non_member, Snippet::INTERNAL, false], - [:internal, ProjectFeature::DISABLED, :non_member, Snippet::PRIVATE, false], + [:internal, :disabled, :non_member, :public, false], + [:internal, :disabled, :non_member, :internal, false], + [:internal, :disabled, :non_member, :private, false], - [:internal, ProjectFeature::DISABLED, :member, Snippet::PUBLIC, false], - [:internal, ProjectFeature::DISABLED, :member, Snippet::INTERNAL, false], - [:internal, ProjectFeature::DISABLED, :member, Snippet::PRIVATE, false], + [:internal, :disabled, :member, :public, false], + [:internal, :disabled, :member, :internal, false], + [:internal, :disabled, :member, :private, false], - [:internal, ProjectFeature::DISABLED, :author, Snippet::PUBLIC, false], - [:internal, ProjectFeature::DISABLED, :author, Snippet::INTERNAL, false], - [:internal, ProjectFeature::DISABLED, :author, Snippet::PRIVATE, false], + [:internal, :disabled, :author, :public, false], + [:internal, :disabled, :author, :internal, false], + [:internal, :disabled, :author, :private, false], # Private projects - [:private, ProjectFeature::ENABLED, :unauthenticated, Snippet::PUBLIC, false], - [:private, ProjectFeature::ENABLED, :unauthenticated, Snippet::INTERNAL, false], - [:private, ProjectFeature::ENABLED, :unauthenticated, Snippet::PRIVATE, false], + [:private, :enabled, :unauthenticated, :public, false], + [:private, :enabled, :unauthenticated, :internal, false], + [:private, :enabled, :unauthenticated, :private, false], - [:private, ProjectFeature::ENABLED, :external, Snippet::PUBLIC, true], - [:private, ProjectFeature::ENABLED, :external, Snippet::INTERNAL, true], - [:private, ProjectFeature::ENABLED, :external, Snippet::PRIVATE, true], + [:private, :enabled, :external, :public, true], + [:private, :enabled, :external, :internal, true], + [:private, :enabled, :external, :private, true], - [:private, ProjectFeature::ENABLED, :non_member, Snippet::PUBLIC, false], - [:private, ProjectFeature::ENABLED, :non_member, Snippet::INTERNAL, false], - [:private, ProjectFeature::ENABLED, :non_member, Snippet::PRIVATE, false], + [:private, :enabled, :non_member, :public, false], + [:private, :enabled, :non_member, :internal, false], + [:private, :enabled, :non_member, :private, false], - [:private, ProjectFeature::ENABLED, :member, Snippet::PUBLIC, true], - [:private, ProjectFeature::ENABLED, :member, Snippet::INTERNAL, true], - [:private, ProjectFeature::ENABLED, :member, Snippet::PRIVATE, true], + [:private, :enabled, :member, :public, true], + [:private, :enabled, :member, :internal, true], + [:private, :enabled, :member, :private, true], - [:private, ProjectFeature::ENABLED, :author, Snippet::PUBLIC, true], - [:private, ProjectFeature::ENABLED, :author, Snippet::INTERNAL, true], - [:private, ProjectFeature::ENABLED, :author, Snippet::PRIVATE, true], + [:private, :enabled, :author, :public, true], + [:private, :enabled, :author, :internal, true], + [:private, :enabled, :author, :private, true], - [:private, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PUBLIC, false], - [:private, ProjectFeature::PRIVATE, :unauthenticated, Snippet::INTERNAL, false], - [:private, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PRIVATE, false], + [:private, :private, :unauthenticated, :public, false], + [:private, :private, :unauthenticated, :internal, false], + [:private, :private, :unauthenticated, :private, false], - [:private, ProjectFeature::PRIVATE, :external, Snippet::PUBLIC, true], - [:private, ProjectFeature::PRIVATE, :external, Snippet::INTERNAL, true], - [:private, ProjectFeature::PRIVATE, :external, Snippet::PRIVATE, true], + [:private, :private, :external, :public, true], + [:private, :private, :external, :internal, true], + [:private, :private, :external, :private, true], - [:private, ProjectFeature::PRIVATE, :non_member, Snippet::PUBLIC, false], - [:private, ProjectFeature::PRIVATE, :non_member, Snippet::INTERNAL, false], - [:private, ProjectFeature::PRIVATE, :non_member, Snippet::PRIVATE, false], + [:private, :private, :non_member, :public, false], + [:private, :private, :non_member, :internal, false], + [:private, :private, :non_member, :private, false], - [:private, ProjectFeature::PRIVATE, :member, Snippet::PUBLIC, true], - [:private, ProjectFeature::PRIVATE, :member, Snippet::INTERNAL, true], - [:private, ProjectFeature::PRIVATE, :member, Snippet::PRIVATE, true], + [:private, :private, :member, :public, true], + [:private, :private, :member, :internal, true], + [:private, :private, :member, :private, true], - [:private, ProjectFeature::PRIVATE, :author, Snippet::PUBLIC, true], - [:private, ProjectFeature::PRIVATE, :author, Snippet::INTERNAL, true], - [:private, ProjectFeature::PRIVATE, :author, Snippet::PRIVATE, true], + [:private, :private, :author, :public, true], + [:private, :private, :author, :internal, true], + [:private, :private, :author, :private, true], - [:private, ProjectFeature::DISABLED, :unauthenticated, Snippet::PUBLIC, false], - [:private, ProjectFeature::DISABLED, :unauthenticated, Snippet::INTERNAL, false], - [:private, ProjectFeature::DISABLED, :unauthenticated, Snippet::PRIVATE, false], + [:private, :disabled, :unauthenticated, :public, false], + [:private, :disabled, :unauthenticated, :internal, false], + [:private, :disabled, :unauthenticated, :private, false], - [:private, ProjectFeature::DISABLED, :external, Snippet::PUBLIC, false], - [:private, ProjectFeature::DISABLED, :external, Snippet::INTERNAL, false], - [:private, ProjectFeature::DISABLED, :external, Snippet::PRIVATE, false], + [:private, :disabled, :external, :public, false], + [:private, :disabled, :external, :internal, false], + [:private, :disabled, :external, :private, false], - [:private, ProjectFeature::DISABLED, :non_member, Snippet::PUBLIC, false], - [:private, ProjectFeature::DISABLED, :non_member, Snippet::INTERNAL, false], - [:private, ProjectFeature::DISABLED, :non_member, Snippet::PRIVATE, false], + [:private, :disabled, :non_member, :public, false], + [:private, :disabled, :non_member, :internal, false], + [:private, :disabled, :non_member, :private, false], - [:private, ProjectFeature::DISABLED, :member, Snippet::PUBLIC, false], - [:private, ProjectFeature::DISABLED, :member, Snippet::INTERNAL, false], - [:private, ProjectFeature::DISABLED, :member, Snippet::PRIVATE, false], + [:private, :disabled, :member, :public, false], + [:private, :disabled, :member, :internal, false], + [:private, :disabled, :member, :private, false], - [:private, ProjectFeature::DISABLED, :author, Snippet::PUBLIC, false], - [:private, ProjectFeature::DISABLED, :author, Snippet::INTERNAL, false], - [:private, ProjectFeature::DISABLED, :author, Snippet::PRIVATE, false] + [:private, :disabled, :author, :public, false], + [:private, :disabled, :author, :internal, false], + [:private, :disabled, :author, :private, false] ] end with_them do - let!(:project_visibility) { project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(project_type.to_s)) } - let!(:project_feature) { project.project_feature.update_column(:snippets_access_level, feature_visibility) } - let!(:user) { users[user_type] } - let!(:snippet) { create(:project_snippet, visibility_level: snippet_type, project: project, author: author) } - let!(:external_member) do - member = project.project_member(external) - - if project.private? - project.add_developer(external) unless member - else - member.delete if member + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.level_value(project_visibility.to_s), snippets_access_level: feature_visibility) + + if user_type == :external + member = project.project_member(external) + + if project.private? + project.add_developer(external) unless member + else + member.delete if member + end end end context "For #{params[:project_type]} project and #{params[:user_type]} users" do - it 'agrees with the read_snippet policy' do + it 'returns proper outcome' do expect(can?(user, :read_snippet, snippet)).to eq(outcome) - end - it 'returns proper outcome' do results = described_class.new(user, project: project).execute expect(results.include?(snippet)).to eq(outcome) @@ -243,16 +256,8 @@ RSpec.shared_examples 'snippet visibility' do context "Without a given project and #{params[:user_type]} users" do it 'returns proper outcome' do results = described_class.new(user).execute - expect(results.include?(snippet)).to eq(outcome) - end - it 'returns no snippets when the user cannot read cross project' do - allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } - - snippets = described_class.new(user).execute - - expect(snippets).to be_empty + expect(results.include?(snippet)).to eq(outcome) end end end @@ -270,46 +275,55 @@ RSpec.shared_examples 'snippet visibility' do where(:snippet_visibility, :user_type, :outcome) do [ - [Snippet::PUBLIC, :unauthenticated, true], - [Snippet::PUBLIC, :external, true], - [Snippet::PUBLIC, :non_member, true], - [Snippet::PUBLIC, :author, true], - - [Snippet::INTERNAL, :unauthenticated, false], - [Snippet::INTERNAL, :external, false], - [Snippet::INTERNAL, :non_member, true], - [Snippet::INTERNAL, :author, true], - - [Snippet::PRIVATE, :unauthenticated, false], - [Snippet::PRIVATE, :external, false], - [Snippet::PRIVATE, :non_member, false], - [Snippet::PRIVATE, :author, true] + [:public, :unauthenticated, true], + [:public, :external, true], + [:public, :non_member, true], + [:public, :author, true], + + [:internal, :unauthenticated, false], + [:internal, :external, false], + [:internal, :non_member, true], + [:internal, :author, true], + + [:private, :unauthenticated, false], + [:private, :external, false], + [:private, :non_member, false], + [:private, :author, true] ] end with_them do - let!(:user) { users[user_type] } - let!(:snippet) { create(:personal_snippet, visibility_level: snippet_visibility, author: author) } + let_it_be(:private_snippet) { create(:personal_snippet, :private, author: author) } + let_it_be(:public_snippet) { create(:personal_snippet, :public, author: author) } + let_it_be(:internal_snippet) { create(:personal_snippet, :internal, author: author) } context "For personal and #{params[:snippet_visibility]} snippets with #{params[:user_type]} user" do - it 'agrees with read_snippet policy' do + it 'returns proper outcome' do expect(can?(user, :read_snippet, snippet)).to eq(outcome) - end - it 'returns proper outcome' do results = described_class.new(user).execute + expect(results.include?(snippet)).to eq(outcome) end + end + end + end - it 'returns personal snippets when the user cannot read cross project' do - allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + context 'when the user cannot read cross project' do + it 'returns only personal snippets' do + personal_snippet = create(:personal_snippet, :public, author: author) + create(:project_snippet, :public, project: project, author: author) - results = described_class.new(user).execute + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(author, :read_cross_project) { false } - expect(results.include?(snippet)).to eq(outcome) - end - end + service = described_class.new(author) + + expect(service).to receive(:personal_snippets).and_call_original + expect(service).not_to receive(:snippets_of_visible_projects) + expect(service).not_to receive(:snippets_of_authorized_projects) + + expect(service.execute).to match_array([personal_snippet]) end end end diff --git a/spec/support/shared_examples/graphql/design_fields_shared_examples.rb b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb index 029d7e677da..ef7086234c4 100644 --- a/spec/support/shared_examples/graphql/design_fields_shared_examples.rb +++ b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb @@ -35,6 +35,7 @@ RSpec.shared_examples 'a GraphQL type with design fields' do object = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id)) object_type.authorized_new(object, query.context) end + let(:instance_b) do object_b = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id_b)) object_type.authorized_new(object_b, query.context) diff --git a/spec/support/shared_examples/graphql/mutations/resolves_subscription_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/resolves_subscription_shared_examples.rb new file mode 100644 index 00000000000..ebba312e895 --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/resolves_subscription_shared_examples.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'a subscribeable graphql resource' do + let(:project) { resource.project } + let_it_be(:user) { create(:user) } + + subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } + + specify { expect(described_class).to require_graphql_authorizations(permission_name) } + + describe '#resolve' do + let(:subscribe) { true } + let(:mutated_resource) { subject[resource.class.name.underscore.to_sym] } + + subject { mutation.resolve(project_path: resource.project.full_path, iid: resource.iid, subscribed_state: subscribe) } + + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + + context 'when the user can update the resource' do + before do + resource.project.add_developer(user) + end + + it 'subscribes to the resource' do + expect(mutated_resource).to eq(resource) + expect(mutated_resource.subscribed?(user, project)).to eq(true) + expect(subject[:errors]).to be_empty + end + + context 'when passing subscribe as false' do + let(:subscribe) { false } + + it 'unsubscribes from the discussion' do + resource.subscribe(user, project) + + expect(mutated_resource.subscribed?(user, project)).to eq(false) + end + end + end + end +end diff --git a/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb new file mode 100644 index 00000000000..cfa12171b7e --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'an assignable resource' do + let_it_be(:user) { create(:user) } + + subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } + + describe '#resolve' do + let_it_be(:assignee) { create(:user) } + let_it_be(:assignee2) { create(:user) } + let(:assignee_usernames) { [assignee.username] } + let(:mutated_resource) { subject[resource.class.name.underscore.to_sym] } + + subject { mutation.resolve(project_path: resource.project.full_path, iid: resource.iid, assignee_usernames: assignee_usernames) } + + before do + resource.project.add_developer(assignee) + resource.project.add_developer(assignee2) + end + + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + + context 'when the user can update the resource' do + before do + resource.project.add_developer(user) + end + + it 'replaces the assignee' do + resource.assignees = [assignee2] + resource.save! + + expect(mutated_resource).to eq(resource) + expect(mutated_resource.assignees).to contain_exactly(assignee) + expect(subject[:errors]).to be_empty + end + + it 'returns errors when resource could not be updated' do + allow(resource).to receive(:errors_on_object).and_return(['foo']) + + expect(subject[:errors]).not_to match_array(['foo']) + end + + context 'when passing an empty assignee list' do + let(:assignee_usernames) { [] } + + before do + resource.assignees = [assignee] + resource.save! + end + + it 'removes all assignees' do + expect(mutated_resource).to eq(resource) + expect(mutated_resource.assignees).to eq([]) + expect(subject[:errors]).to be_empty + end + end + + context 'when passing "append" as true' do + subject do + mutation.resolve( + project_path: resource.project.full_path, + iid: resource.iid, + assignee_usernames: assignee_usernames, + operation_mode: Types::MutationOperationModeEnum.enum[:append] + ) + end + + before do + resource.assignees = [assignee2] + resource.save! + + # In CE, APPEND is a NOOP as you can't have multiple assignees + # We test multiple assignment in EE specs + if resource.is_a?(MergeRequest) + stub_licensed_features(multiple_merge_request_assignees: false) + else + stub_licensed_features(multiple_issue_assignees: false) + end + end + + it 'is a NO-OP in FOSS' do + expect(mutated_resource).to eq(resource) + expect(mutated_resource.assignees).to contain_exactly(assignee2) + expect(subject[:errors]).to be_empty + end + end + + context 'when passing "remove" as true' do + before do + resource.assignees = [assignee] + resource.save! + end + + it 'removes named assignee' do + mutated_resource = mutation.resolve( + project_path: resource.project.full_path, + iid: resource.iid, + assignee_usernames: assignee_usernames, + operation_mode: Types::MutationOperationModeEnum.enum[:remove] + )[resource.class.name.underscore.to_sym] + + expect(mutated_resource).to eq(resource) + expect(mutated_resource.assignees).to eq([]) + expect(subject[:errors]).to be_empty + end + + it 'does not remove unnamed assignee' do + mutated_resource = mutation.resolve( + project_path: resource.project.full_path, + iid: resource.iid, + assignee_usernames: [assignee2.username], + operation_mode: Types::MutationOperationModeEnum.enum[:remove] + )[resource.class.name.underscore.to_sym] + + expect(mutated_resource).to eq(resource) + expect(mutated_resource.assignees).to contain_exactly(assignee) + expect(subject[:errors]).to be_empty + end + end + end + end +end diff --git a/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb b/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb index e1dd98814f1..41b7da07d2d 100644 --- a/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb +++ b/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb @@ -8,6 +8,7 @@ RSpec.shared_context 'exposing regular notes on a noteable in GraphQL' do noteable: noteable, project: (noteable.project if noteable.respond_to?(:project))) end + let(:user) { note.author } context 'for regular notes' do diff --git a/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb b/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb new file mode 100644 index 00000000000..397e22ace28 --- /dev/null +++ b/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +shared_examples 'N+1 query check' do + it 'prevents N+1 queries' do + execute_query # "warm up" to prevent undeterministic counts + + control_count = ActiveRecord::QueryRecorder.new { execute_query }.count + + search_params[:iids] << extra_iid_for_second_query + expect { execute_query }.not_to exceed_query_limit(control_count) + end +end diff --git a/spec/support/shared_examples/lib/api/ci/runner_shared_examples.rb b/spec/support/shared_examples/lib/api/ci/runner_shared_examples.rb new file mode 100644 index 00000000000..bdb0316bf5a --- /dev/null +++ b/spec/support/shared_examples/lib/api/ci/runner_shared_examples.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'API::CI::Runner application context metadata' do |api_route| + it 'contains correct context metadata' do + # Avoids popping the context from the thread so we can + # check its content after the request. + allow(Labkit::Context).to receive(:pop) + + send_request + + Labkit::Context.with_context do |context| + expected_context = { + 'meta.caller_id' => api_route, + 'meta.user' => job.user.username, + 'meta.project' => job.project.full_path, + 'meta.root_namespace' => job.project.full_path_components.first + } + + expect(context.to_h).to include(expected_context) + end + 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 af65b61021c..8cf6babe146 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 @@ -82,3 +82,25 @@ RSpec.shared_examples 'schedules resource mentions migration' do |resource_class end end end + +RSpec.shared_examples 'resource migration not run' do |migration_class, resource_class| + it 'does not migrate mentions' do + join = migration_class::JOIN + conditions = migration_class::QUERY_CONDITIONS + + expect do + 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| + 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)) + end.to change { user_mentions.count }.by(0) + end +end diff --git a/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb new file mode 100644 index 00000000000..a3800f050bb --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'network policy common specs' do + let(:name) { 'example-name' } + let(:namespace) { 'example-namespace' } + let(:labels) { nil } + + describe 'as_json' do + let(:json_policy) do + { + name: name, + namespace: namespace, + creation_timestamp: nil, + manifest: YAML.dump( + { + metadata: metadata, + spec: spec + }.deep_stringify_keys + ), + is_autodevops: false, + is_enabled: true + } + end + + subject { policy.as_json } + + it { is_expected.to eq(json_policy) } + end + + describe 'autodevops?' do + subject { policy.autodevops? } + + let(:labels) { { chart: chart } } + let(:chart) { nil } + + it { is_expected.to be false } + + context 'with non-autodevops chart' do + let(:chart) { 'foo' } + + it { is_expected.to be false } + end + + context 'with autodevops chart' do + let(:chart) { 'auto-deploy-app-0.6.0' } + + it { is_expected.to be true } + end + end + + describe 'enabled?' do + subject { policy.enabled? } + + let(:selector) { nil } + + it { is_expected.to be true } + + context 'with empty selector' do + let(:selector) { {} } + + it { is_expected.to be true } + end + + context 'with nil matchLabels in selector' do + let(:selector) { { matchLabels: nil } } + + it { is_expected.to be true } + end + + context 'with empty matchLabels in selector' do + let(:selector) { { matchLabels: {} } } + + it { is_expected.to be true } + end + + context 'with disabled_by label in matchLabels in selector' do + let(:selector) do + { matchLabels: { Gitlab::Kubernetes::NetworkPolicyCommon::DISABLED_BY_LABEL => 'gitlab' } } + end + + it { is_expected.to be false } + end + end + + describe 'enable' do + subject { policy.enabled? } + + let(:selector) { nil } + + before do + policy.enable + end + + it { is_expected.to be true } + + context 'with empty selector' do + let(:selector) { {} } + + it { is_expected.to be true } + end + + context 'with nil matchLabels in selector' do + let(:selector) { { matchLabels: nil } } + + it { is_expected.to be true } + end + + context 'with empty matchLabels in selector' do + let(:selector) { { matchLabels: {} } } + + it { is_expected.to be true } + end + + context 'with disabled_by label in matchLabels in selector' do + let(:selector) do + { matchLabels: { Gitlab::Kubernetes::NetworkPolicyCommon::DISABLED_BY_LABEL => 'gitlab' } } + end + + it { is_expected.to be true } + end + end + + describe 'disable' do + subject { policy.enabled? } + + let(:selector) { nil } + + before do + policy.disable + end + + it { is_expected.to be false } + + context 'with empty selector' do + let(:selector) { {} } + + it { is_expected.to be false } + end + + context 'with nil matchLabels in selector' do + let(:selector) { { matchLabels: nil } } + + it { is_expected.to be false } + end + + context 'with empty matchLabels in selector' do + let(:selector) { { matchLabels: {} } } + + it { is_expected.to be false } + end + + context 'with disabled_by label in matchLabels in selector' do + let(:selector) do + { matchLabels: { Gitlab::Kubernetes::NetworkPolicyCommon::DISABLED_BY_LABEL => 'gitlab' } } + end + + it { is_expected.to be false } + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb new file mode 100644 index 00000000000..6b6e25ca1dd --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'file template shared examples' do |filename, file_extension| + describe '.all' do + it "strips the #{file_extension} suffix" do + expect(subject.all.first.name).not_to end_with(file_extension) + end + + it 'ensures that the template name is used exactly once' do + all = subject.all.group_by(&:name) + duplicates = all.select { |_, templates| templates.length > 1 } + + expect(duplicates).to be_empty + end + end + + describe '.by_category' do + it 'returns sorted results' do + result = described_class.by_category('General') + + expect(result).to eq(result.sort) + end + end + + describe '.find' do + it 'returns nil if the file does not exist' do + expect(subject.find('nonexistent-file')).to be nil + end + + it 'returns the corresponding object of a valid file' do + template = subject.find(filename) + + expect(template).to be_a described_class + expect(template.name).to eq(filename) + end + end + + describe '#<=>' do + it 'sorts lexicographically' do + one = described_class.new("a.#{file_extension}") + other = described_class.new("z.#{file_extension}") + + expect(one.<=>(other)).to be(-1) + expect([other, one].sort).to eq([one, other]) + end + end +end diff --git a/spec/support/shared_examples/models/chat_service_shared_examples.rb b/spec/support/shared_examples/models/chat_service_shared_examples.rb index 0a1c27b32db..ad237ad9f49 100644 --- a/spec/support/shared_examples/models/chat_service_shared_examples.rb +++ b/spec/support/shared_examples/models/chat_service_shared_examples.rb @@ -198,6 +198,7 @@ RSpec.shared_examples "chat service" do |service_name| message: "user created page: Awesome wiki_page" } end + let(:wiki_page) { create(:wiki_page, wiki: project.wiki, **opts) } let(:sample_data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, "create") } @@ -250,6 +251,7 @@ RSpec.shared_examples "chat service" do |service_name| project: project, status: status, sha: project.commit.sha, ref: project.default_branch) end + let(:sample_data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } context "with failed pipeline" do diff --git a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb index 239588d3b2f..394253fb699 100644 --- a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb @@ -28,46 +28,16 @@ RSpec.shared_examples 'cluster application helm specs' do |application_name| describe '#files' do subject { application.files } - context 'managed_apps_local_tiller feature flag is disabled' do - before do - stub_feature_flags(managed_apps_local_tiller: false) - end - - context 'when the helm application does not have a ca_cert' do - before do - application.cluster.application_helm.ca_cert = nil - end - - it 'does not include cert files when there is no ca_cert entry' do - expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem') - end - end - - it 'includes cert files when there is a ca_cert entry' do - expect(subject).to include(:'ca.pem', :'cert.pem', :'key.pem') - expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert) - - cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem']) - expect(cert.not_after).to be < 60.minutes.from_now - end + it 'does not include cert files' do + expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem') end - context 'managed_apps_local_tiller feature flag is enabled' do - before do - stub_feature_flags(managed_apps_local_tiller: application.cluster.clusterable) - end + context 'when cluster does not have helm installed' do + let(:application) { create(application_name, :no_helm_installed) } it 'does not include cert files' do expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem') end - - context 'when cluster does not have helm installed' do - let(:application) { create(application_name, :no_helm_installed) } - - it 'does not include cert files' do - expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem') - end - end end end end diff --git a/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb index 7f0c60d4204..55e458db512 100644 --- a/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb @@ -6,46 +6,8 @@ RSpec.shared_examples 'cluster application initial status specs' do subject { described_class.new(cluster: cluster) } - context 'local tiller feature flag is disabled' do - before do - stub_feature_flags(managed_apps_local_tiller: false) - end - - it 'sets a default status' do - expect(subject.status_name).to be(:not_installable) - end - end - - context 'local tiller feature flag is enabled' do - before do - stub_feature_flags(managed_apps_local_tiller: cluster.clusterable) - end - - it 'sets a default status' do - expect(subject.status_name).to be(:installable) - end - end - - context 'when application helm is scheduled' do - before do - stub_feature_flags(managed_apps_local_tiller: false) - - create(:clusters_applications_helm, :scheduled, cluster: cluster) - end - - it 'defaults to :not_installable' do - expect(subject.status_name).to be(:not_installable) - end - end - - context 'when application helm is installed' do - before do - create(:clusters_applications_helm, :installed, cluster: cluster) - end - - it 'sets a default status' do - expect(subject.status_name).to be(:installable) - end + it 'sets a default status' do + expect(subject.status_name).to be(:installable) end end end diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb index f80ca235220..7603787a54e 100644 --- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -48,43 +48,21 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| expect(subject).to be_installed end - context 'managed_apps_local_tiller feature flag disabled' do - before do - stub_feature_flags(managed_apps_local_tiller: false) - end - - it 'updates helm version' do - subject.cluster.application_helm.update!(version: '1.2.3') + it 'does not update the helm version' do + subject.cluster.application_helm.update!(version: '1.2.3') + expect do subject.make_installed! subject.cluster.application_helm.reload - - expect(subject.cluster.application_helm.version).to eq(Gitlab::Kubernetes::Helm::HELM_VERSION) - end + end.not_to change { subject.cluster.application_helm.version } end - context 'managed_apps_local_tiller feature flag enabled' do - before do - stub_feature_flags(managed_apps_local_tiller: subject.cluster.clusterable) - end - - it 'does not update the helm version' do - subject.cluster.application_helm.update!(version: '1.2.3') - - expect do - subject.make_installed! - - subject.cluster.application_helm.reload - end.not_to change { subject.cluster.application_helm.version } - end - - context 'the cluster has no helm installed' do - subject { create(application_name, :installing, :no_helm_installed) } + context 'the cluster has no helm installed' do + subject { create(application_name, :installing, :no_helm_installed) } - it 'runs without errors' do - expect { subject.make_installed! }.not_to raise_error - end + it 'runs without errors' do + expect { subject.make_installed! }.not_to raise_error end end @@ -97,43 +75,21 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| expect(subject).to be_updated end - context 'managed_apps_local_tiller feature flag disabled' do - before do - stub_feature_flags(managed_apps_local_tiller: false) - end - - it 'updates helm version' do - subject.cluster.application_helm.update!(version: '1.2.3') + it 'does not update the helm version' do + subject.cluster.application_helm.update!(version: '1.2.3') + expect do subject.make_installed! subject.cluster.application_helm.reload - - expect(subject.cluster.application_helm.version).to eq(Gitlab::Kubernetes::Helm::HELM_VERSION) - end + end.not_to change { subject.cluster.application_helm.version } end - context 'managed_apps_local_tiller feature flag enabled' do - before do - stub_feature_flags(managed_apps_local_tiller: true) - end - - it 'does not update the helm version' do - subject.cluster.application_helm.update!(version: '1.2.3') - - expect do - subject.make_installed! - - subject.cluster.application_helm.reload - end.not_to change { subject.cluster.application_helm.version } - end - - context 'the cluster has no helm installed' do - subject { create(application_name, :updating, :no_helm_installed) } + context 'the cluster has no helm installed' do + subject { create(application_name, :updating, :no_helm_installed) } - it 'runs without errors' do - expect { subject.make_installed! }.not_to raise_error - end + it 'runs without errors' do + expect { subject.make_installed! }.not_to raise_error end end end @@ -185,62 +141,26 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| expect(subject).to be_installed end - context 'local tiller flag enabled' do - before do - stub_feature_flags(managed_apps_local_tiller: true) - end - - context 'helm record does not exist' do - subject { build(application_name, :installing, :no_helm_installed) } - - it 'does not create a helm record' do - subject.make_externally_installed! - - subject.cluster.reload - expect(subject.cluster.application_helm).to be_nil - end - end - - context 'helm record exists' do - subject { build(application_name, :installing, cluster: old_helm.cluster) } + context 'helm record does not exist' do + subject { build(application_name, :installing, :no_helm_installed) } - it 'does not update helm version' do - subject.make_externally_installed! + it 'does not create a helm record' do + subject.make_externally_installed! - subject.cluster.application_helm.reload - - expect(subject.cluster.application_helm.version).to eq('1.2.3') - end + subject.cluster.reload + expect(subject.cluster.application_helm).to be_nil end end - context 'local tiller flag disabled' do - before do - stub_feature_flags(managed_apps_local_tiller: false) - end - - context 'helm record does not exist' do - subject { build(application_name, :installing, :no_helm_installed) } - - it 'creates a helm record' do - subject.make_externally_installed! - - subject.cluster.reload - expect(subject.cluster.application_helm).to be_present - expect(subject.cluster.application_helm).to be_persisted - end - end - - context 'helm record exists' do - subject { build(application_name, :installing, cluster: old_helm.cluster) } + context 'helm record exists' do + subject { build(application_name, :installing, cluster: old_helm.cluster) } - it 'does not update helm version' do - subject.make_externally_installed! + it 'does not update helm version' do + subject.make_externally_installed! - subject.cluster.application_helm.reload + subject.cluster.application_helm.reload - expect(subject.cluster.application_helm.version).to eq('1.2.3') - end + expect(subject.cluster.application_helm.version).to eq('1.2.3') end end @@ -262,6 +182,14 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| expect(subject).to be_installed end + + it 'clears #status_reason' do + expect(subject.status_reason).not_to be_nil + + subject.make_externally_installed! + + expect(subject.status_reason).to be_nil + end end end @@ -292,6 +220,14 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| expect(subject).to be_uninstalled end + + it 'clears #status_reason' do + expect(subject.status_reason).not_to be_nil + + subject.make_externally_uninstalled! + + expect(subject.status_reason).to be_nil + end end end diff --git a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb new file mode 100644 index 00000000000..99a09993900 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.shared_examples_for CounterAttribute do |counter_attributes| + it 'defines a Redis counter_key' do + expect(model.counter_key(:counter_name)) + .to eq("project:{#{model.project_id}}:counters:CounterAttributeModel:#{model.id}:counter_name") + end + + it 'defines a method to store counters' do + expect(model.class.counter_attributes.to_a).to eq(counter_attributes) + end + + counter_attributes.each do |attribute| + describe attribute do + describe '#delayed_increment_counter', :redis do + let(:increment) { 10 } + + subject { model.delayed_increment_counter(attribute, increment) } + + context 'when attribute is a counter attribute' do + where(:increment) { [10, -3] } + + with_them do + it 'increments the counter in Redis' do + subject + + Gitlab::Redis::SharedState.with do |redis| + counter = redis.get(model.counter_key(attribute)) + expect(counter).to eq(increment.to_s) + end + end + + it 'does not increment the counter for the record' do + expect { subject }.not_to change { model.reset.read_attribute(attribute) } + end + + it 'schedules a worker to flush counter increments asynchronously' do + expect(FlushCounterIncrementsWorker).to receive(:perform_in) + .with(CounterAttribute::WORKER_DELAY, model.class.name, model.id, attribute) + .and_call_original + + subject + end + end + + context 'when increment is 0' do + let(:increment) { 0 } + + it 'does nothing' do + expect(FlushCounterIncrementsWorker).not_to receive(:perform_in) + expect(model).not_to receive(:update!) + + subject + end + end + end + + context 'when attribute is not a counter attribute' do + it 'delegates to ActiveRecord update!' do + expect { model.delayed_increment_counter(:unknown_attribute, 10) } + .to raise_error(ActiveModel::MissingAttributeError) + end + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(efficient_counter_attribute: false) + end + + it 'delegates to ActiveRecord update!' do + expect { subject } + .to change { model.reset.read_attribute(attribute) }.by(increment) + end + + it 'does not increment the counter in Redis' do + subject + + Gitlab::Redis::SharedState.with do |redis| + counter = redis.get(model.counter_key(attribute)) + expect(counter).to be_nil + end + end + end + end + end + end + + describe '.flush_increments_to_database!', :redis do + let(:incremented_attribute) { counter_attributes.first } + + subject { model.flush_increments_to_database!(incremented_attribute) } + + it 'obtains an exclusive lease during processing' do + expect(model) + .to receive(:in_lock) + .with(model.counter_lock_key(incremented_attribute), ttl: described_class::WORKER_LOCK_TTL) + .and_call_original + + subject + end + + context 'when there is a counter to flush' do + before do + model.delayed_increment_counter(incremented_attribute, 10) + model.delayed_increment_counter(incremented_attribute, -3) + end + + it 'updates the record' do + expect { subject }.to change { model.reset.read_attribute(incremented_attribute) }.by(7) + end + + it 'removes the increment entry from Redis' do + Gitlab::Redis::SharedState.with do |redis| + key_exists = redis.exists(model.counter_key(incremented_attribute)) + expect(key_exists).to be_truthy + end + + subject + + Gitlab::Redis::SharedState.with do |redis| + key_exists = redis.exists(model.counter_key(incremented_attribute)) + expect(key_exists).to be_falsey + end + end + end + + context 'when there are no counters to flush' do + context 'when there are no counters in the relative :flushed key' do + it 'does not change the record' do + expect { subject }.not_to change { model.reset.attributes } + end + end + + # This can be the case where updating counters in the database fails with error + # and retrying the worker will retry flushing the counters but the main key has + # disappeared and the increment has been moved to the "<...>:flushed" key. + context 'when there are counters in the relative :flushed key' do + before do + Gitlab::Redis::SharedState.with do |redis| + redis.incrby(model.counter_flushed_key(incremented_attribute), 10) + end + end + + it 'updates the record' do + expect { subject }.to change { model.reset.read_attribute(incremented_attribute) }.by(10) + end + + it 'deletes the relative :flushed key' do + subject + + Gitlab::Redis::SharedState.with do |redis| + key_exists = redis.exists(model.counter_flushed_key(incremented_attribute)) + expect(key_exists).to be_falsey + end + end + end + end + + context 'when deleting :flushed key fails' do + before do + Gitlab::Redis::SharedState.with do |redis| + redis.incrby(model.counter_flushed_key(incremented_attribute), 10) + + expect(redis).to receive(:del).and_raise('could not delete key') + end + end + + it 'does a rollback of the counter update' do + expect { subject }.to raise_error('could not delete key') + + expect(model.reset.read_attribute(incremented_attribute)).to eq(0) + end + end + end +end diff --git a/spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb b/spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb new file mode 100644 index 00000000000..4cb087c47ad --- /dev/null +++ b/spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'mounted file in local store' do + it 'is stored locally' do + expect(subject.file_store).to be(ObjectStorage::Store::LOCAL) + expect(subject.file).to be_file_storage + expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL) + end +end + +RSpec.shared_examples 'mounted file in object store' do + it 'is stored remotely' do + expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE) + expect(subject.file).not_to be_file_storage + expect(subject.file.object_store).to eq(ObjectStorage::Store::REMOTE) + 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 32d502af5a2..15ca1f56bd0 100644 --- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb @@ -3,7 +3,8 @@ RSpec.shared_examples 'a timebox' do |timebox_type| let(:project) { create(:project, :public) } let(:group) { create(:group) } - let(:timebox) { create(timebox_type, project: project) } + let(:timebox_args) { [] } + let(:timebox) { create(timebox_type, *timebox_args, project: project) } let(:issue) { create(:issue, project: project) } let(:user) { create(:user) } let(:timebox_table_name) { timebox_type.to_s.pluralize.to_sym } @@ -12,7 +13,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| context 'with a project' do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } - let(:instance) { build(timebox_type, project: build(:project), group: nil) } + let(:instance) { build(timebox_type, *timebox_args, project: build(:project), group: nil) } let(:scope) { :project } let(:scope_attrs) { { project: instance.project } } let(:usage) { timebox_table_name } @@ -22,7 +23,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| context 'with a group' do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } - let(:instance) { build(timebox_type, project: nil, group: build(:group)) } + let(:instance) { build(timebox_type, *timebox_args, project: nil, group: build(:group)) } let(:scope) { :group } let(:scope_attrs) { { namespace: instance.group } } let(:usage) { timebox_table_name } @@ -37,14 +38,14 @@ RSpec.shared_examples 'a timebox' do |timebox_type| describe 'start_date' do it 'adds an error when start_date is greater then due_date' do - timebox = build(timebox_type, start_date: Date.tomorrow, due_date: Date.yesterday) + timebox = build(timebox_type, *timebox_args, start_date: Date.tomorrow, due_date: Date.yesterday) expect(timebox).not_to be_valid expect(timebox.errors[:due_date]).to include("must be greater than start date") end it 'adds an error when start_date is greater than 9999-12-31' do - timebox = build(timebox_type, start_date: Date.new(10000, 1, 1)) + timebox = build(timebox_type, *timebox_args, start_date: Date.new(10000, 1, 1)) expect(timebox).not_to be_valid expect(timebox.errors[:start_date]).to include("date must not be after 9999-12-31") @@ -53,7 +54,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| describe 'due_date' do it 'adds an error when due_date is greater than 9999-12-31' do - timebox = build(timebox_type, due_date: Date.new(10000, 1, 1)) + timebox = build(timebox_type, *timebox_args, due_date: Date.new(10000, 1, 1)) expect(timebox).not_to be_valid expect(timebox.errors[:due_date]).to include("date must not be after 9999-12-31") @@ -64,7 +65,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| it { is_expected.to validate_presence_of(:title) } it 'is invalid if title would be empty after sanitation' do - timebox = build(timebox_type, project: project, title: '<img src=x onerror=prompt(1)>') + timebox = build(timebox_type, *timebox_args, project: project, title: '<img src=x onerror=prompt(1)>') expect(timebox).not_to be_valid expect(timebox.errors[:title]).to include("can't be blank") @@ -73,7 +74,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| describe '#timebox_type_check' do it 'is invalid if it has both project_id and group_id' do - timebox = build(timebox_type, group: group) + timebox = build(timebox_type, *timebox_args, group: group) timebox.project = project expect(timebox).not_to be_valid @@ -98,7 +99,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end context "per group" do - let(:timebox) { create(timebox_type, group: group) } + let(:timebox) { create(timebox_type, *timebox_args, group: group) } before do project.update(group: group) @@ -111,7 +112,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end it "does not accept the same title of a child project timebox" do - create(timebox_type, project: group.projects.first) + create(timebox_type, *timebox_args, project: group.projects.first) new_timebox = described_class.new(group: group, title: timebox.title) @@ -143,7 +144,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end context 'when project_id is not present' do - let(:timebox) { build(timebox_type, group: group) } + let(:timebox) { build(timebox_type, *timebox_args, group: group) } it 'returns false' do expect(timebox.project_timebox?).to be_falsey @@ -153,7 +154,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| describe '#group_timebox?' do context 'when group_id is present' do - let(:timebox) { build(timebox_type, group: group) } + let(:timebox) { build(timebox_type, *timebox_args, group: group) } it 'returns true' do expect(timebox.group_timebox?).to be_truthy @@ -168,7 +169,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end describe '#safe_title' do - let(:timebox) { create(timebox_type, title: "<b>foo & bar -> 2.2</b>") } + let(:timebox) { create(timebox_type, *timebox_args, title: "<b>foo & bar -> 2.2</b>") } it 'normalizes the title for use as a slug' do expect(timebox.safe_title).to eq('foo-bar-22') @@ -177,7 +178,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| describe '#resource_parent' do context 'when group is present' do - let(:timebox) { build(timebox_type, group: group) } + let(:timebox) { build(timebox_type, *timebox_args, group: group) } it 'returns the group' do expect(timebox.resource_parent).to eq(group) @@ -192,7 +193,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end describe "#title" do - let(:timebox) { create(timebox_type, title: "<b>foo & bar -> 2.2</b>") } + let(:timebox) { create(timebox_type, *timebox_args, title: "<b>foo & bar -> 2.2</b>") } it "sanitizes title" do expect(timebox.title).to eq("foo & bar -> 2.2") @@ -203,28 +204,28 @@ RSpec.shared_examples 'a timebox' do |timebox_type| context "per project" do it "is true for projects with MRs enabled" do project = create(:project, :merge_requests_enabled) - timebox = create(timebox_type, project: project) + timebox = create(timebox_type, *timebox_args, project: project) expect(timebox.merge_requests_enabled?).to be_truthy end it "is false for projects with MRs disabled" do project = create(:project, :repository_enabled, :merge_requests_disabled) - timebox = create(timebox_type, project: project) + timebox = create(timebox_type, *timebox_args, project: project) expect(timebox.merge_requests_enabled?).to be_falsey end it "is false for projects with repository disabled" do project = create(:project, :repository_disabled) - timebox = create(timebox_type, project: project) + timebox = create(timebox_type, *timebox_args, project: project) expect(timebox.merge_requests_enabled?).to be_falsey end end context "per group" do - let(:timebox) { create(timebox_type, group: group) } + let(:timebox) { create(timebox_type, *timebox_args, group: group) } it "is always true for groups, for performance reasons" do expect(timebox.merge_requests_enabled?).to be_truthy @@ -234,7 +235,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| describe '#to_ability_name' do it 'returns timebox' do - timebox = build(timebox_type) + timebox = build(timebox_type, *timebox_args) expect(timebox.to_ability_name).to eq(timebox_type.to_s) end diff --git a/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb index 21ab9b06c33..13ffc1b7f87 100644 --- a/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb +++ b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb @@ -38,6 +38,7 @@ RSpec.shared_examples 'issuable hook data' do |kind| title_html: %w[foo bar] } end + let(:data) { builder.build(user: user, changes: changes) } it 'populates the :changes hash' do 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 99e62ebf422..e4668926d74 100644 --- a/spec/support/shared_examples/models/relative_positioning_shared_examples.rb +++ b/spec/support/shared_examples/models/relative_positioning_shared_examples.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true RSpec.shared_examples 'a class that supports relative positioning' do - let(:item1) { create(factory, default_params) } - let(:item2) { create(factory, default_params) } - let(:new_item) { create(factory, default_params) } + let(:item1) { create_item } + let(:item2) { create_item } + let(:new_item) { create_item } - def create_item(params) + def create_item(params = {}) create(factory, params.merge(default_params)) end @@ -16,31 +16,119 @@ RSpec.shared_examples 'a class that supports relative positioning' do end describe '.move_nulls_to_end' do + let(:item3) { create_item } + it 'moves items with null relative_position to the end' do + item1.update!(relative_position: 1000) + item2.update!(relative_position: nil) + item3.update!(relative_position: nil) + + items = [item1, item2, item3] + expect(described_class.move_nulls_to_end(items)).to be(2) + + expect(items.sort_by(&:relative_position)).to eq(items) + expect(item1.relative_position).to be(1000) + expect(item1.prev_relative_position).to be_nil + expect(item1.next_relative_position).to eq(item2.relative_position) + expect(item2.next_relative_position).to eq(item3.relative_position) + expect(item3.next_relative_position).to be_nil + end + + it 'preserves relative position' do item1.update!(relative_position: nil) item2.update!(relative_position: nil) described_class.move_nulls_to_end([item1, item2]) - expect(item2.prev_relative_position).to eq item1.relative_position - expect(item1.prev_relative_position).to eq nil - expect(item2.next_relative_position).to eq nil + expect(item1.relative_position).to be < item2.relative_position end it 'moves the item near the start position when there are no existing positions' do item1.update!(relative_position: nil) described_class.move_nulls_to_end([item1]) - - expect(item1.relative_position).to eq(described_class::START_POSITION + described_class::IDEAL_DISTANCE) + expect(item1.reset.relative_position).to eq(described_class::START_POSITION + described_class::IDEAL_DISTANCE) end it 'does not perform any moves if all items have their relative_position set' do item1.update!(relative_position: 1) - expect(item1).not_to receive(:save) + expect(described_class.move_nulls_to_start([item1])).to be(0) + expect(item1.reload.relative_position).to be(1) + end + + it 'manages to move nulls to the end even if there is a sequence at the end' do + bunch = create_items_with_positions(run_at_end) + item1.update!(relative_position: nil) described_class.move_nulls_to_end([item1]) + + items = [*bunch, item1] + items.each(&:reset) + + expect(items.map(&:relative_position)).to all(be_valid_position) + expect(items.sort_by(&:relative_position)).to eq(items) + 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]) + + baseline = ActiveRecord::QueryRecorder.new do + described_class.move_nulls_to_end([a, e]) + end + + expect { described_class.move_nulls_to_end([b, c, d]) } + .not_to exceed_query_limit(baseline) + + expect { described_class.move_nulls_to_end([f]) } + .not_to exceed_query_limit(baseline.count) + end + end + + describe '.move_nulls_to_start' do + let(:item3) { create_item } + + it 'moves items with null relative_position to the start' do + item1.update!(relative_position: nil) + item2.update!(relative_position: nil) + item3.update!(relative_position: 1000) + + items = [item1, item2, item3] + expect(described_class.move_nulls_to_start(items)).to be(2) + items.map(&:reload) + + expect(items.sort_by(&:relative_position)).to eq(items) + expect(item1.prev_relative_position).to eq nil + expect(item1.next_relative_position).to eq item2.relative_position + expect(item2.next_relative_position).to eq item3.relative_position + expect(item3.next_relative_position).to eq nil + expect(item3.relative_position).to be(1000) + end + + it 'moves the item near the start position when there are no existing positions' do + item1.update!(relative_position: nil) + + described_class.move_nulls_to_start([item1]) + + expect(item1.relative_position).to eq(described_class::START_POSITION - described_class::IDEAL_DISTANCE) + end + + it 'preserves relative position' do + item1.update!(relative_position: nil) + item2.update!(relative_position: nil) + + described_class.move_nulls_to_start([item1, item2]) + + expect(item1.relative_position).to be < item2.relative_position + end + + it 'does not perform any moves if all items have their relative_position set' do + item1.update!(relative_position: 1) + + expect(described_class.move_nulls_to_start([item1])).to be(0) + expect(item1.reload.relative_position).to be(1) end end @@ -52,8 +140,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do describe '#prev_relative_position' do it 'returns previous position if there is an item above' do - item1.update(relative_position: 5) - item2.update(relative_position: 15) + item1.update!(relative_position: 5) + item2.update!(relative_position: 15) expect(item2.prev_relative_position).to eq item1.relative_position end @@ -65,8 +153,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do describe '#next_relative_position' do it 'returns next position if there is an item below' do - item1.update(relative_position: 5) - item2.update(relative_position: 15) + item1.update!(relative_position: 5) + item2.update!(relative_position: 15) expect(item1.next_relative_position).to eq item2.relative_position end @@ -76,9 +164,172 @@ RSpec.shared_examples 'a class that supports relative positioning' do end end + describe '#find_next_gap_before' do + context 'there is no gap' do + let(:items) { create_items_with_positions(run_at_start) } + + it 'returns nil' do + items.each do |item| + expect(item.send(:find_next_gap_before)).to be_nil + end + end + end + + context 'there is a sequence ending at MAX_POSITION' do + let(:items) { create_items_with_positions(run_at_end) } + + let(:gaps) do + items.map { |item| item.send(:find_next_gap_before) } + end + + it 'can find the gap at the start for any item in the sequence' do + gap = { start: items.first.relative_position, end: RelativePositioning::MIN_POSITION } + + expect(gaps).to all(eq(gap)) + end + + it 'respects lower bounds' do + gap = { start: items.first.relative_position, end: 10 } + new_item.update!(relative_position: 10) + + expect(gaps).to all(eq(gap)) + end + end + + specify do + item1.update!(relative_position: 5) + + (0..10).each do |pos| + item2.update!(relative_position: pos) + + gap = item2.send(:find_next_gap_before) + + expect(gap[:start]).to be <= item2.relative_position + expect((gap[:end] - gap[:start]).abs).to be >= RelativePositioning::MIN_GAP + expect(gap[:start]).to be_valid_position + expect(gap[:end]).to be_valid_position + end + end + + it 'deals with there not being any items to the left' do + create_items_with_positions([1, 2, 3]) + new_item.update!(relative_position: 0) + + expect(new_item.send(:find_next_gap_before)).to eq(start: 0, end: RelativePositioning::MIN_POSITION) + end + + it 'finds the next gap to the left, skipping adjacent values' do + create_items_with_positions([1, 9, 10]) + new_item.update!(relative_position: 11) + + expect(new_item.send(:find_next_gap_before)).to eq(start: 9, end: 1) + end + + it 'finds the next gap to the left' do + create_items_with_positions([2, 10]) + + new_item.update!(relative_position: 15) + expect(new_item.send(:find_next_gap_before)).to eq(start: 15, end: 10) + + new_item.update!(relative_position: 11) + expect(new_item.send(:find_next_gap_before)).to eq(start: 10, end: 2) + + new_item.update!(relative_position: 9) + expect(new_item.send(:find_next_gap_before)).to eq(start: 9, end: 2) + + new_item.update!(relative_position: 5) + expect(new_item.send(:find_next_gap_before)).to eq(start: 5, end: 2) + end + end + + describe '#find_next_gap_after' do + context 'there is no gap' do + let(:items) { create_items_with_positions(run_at_end) } + + it 'returns nil' do + items.each do |item| + expect(item.send(:find_next_gap_after)).to be_nil + end + end + end + + context 'there is a sequence starting at MIN_POSITION' do + let(:items) { create_items_with_positions(run_at_start) } + + let(:gaps) do + items.map { |item| item.send(:find_next_gap_after) } + end + + it 'can find the gap at the end for any item in the sequence' do + gap = { start: items.last.relative_position, end: RelativePositioning::MAX_POSITION } + + expect(gaps).to all(eq(gap)) + end + + it 'respects upper bounds' do + gap = { start: items.last.relative_position, end: 10 } + new_item.update!(relative_position: 10) + + expect(gaps).to all(eq(gap)) + end + end + + specify do + item1.update!(relative_position: 5) + + (0..10).each do |pos| + item2.update!(relative_position: pos) + + gap = item2.send(:find_next_gap_after) + + expect(gap[:start]).to be >= item2.relative_position + expect((gap[:end] - gap[:start]).abs).to be >= RelativePositioning::MIN_GAP + expect(gap[:start]).to be_valid_position + expect(gap[:end]).to be_valid_position + end + end + + it 'deals with there not being any items to the right' do + create_items_with_positions([1, 2, 3]) + new_item.update!(relative_position: 5) + + expect(new_item.send(:find_next_gap_after)).to eq(start: 5, end: RelativePositioning::MAX_POSITION) + end + + it 'finds the next gap to the right, skipping adjacent values' do + create_items_with_positions([1, 2, 10]) + new_item.update!(relative_position: 0) + + expect(new_item.send(:find_next_gap_after)).to eq(start: 2, end: 10) + end + + it 'finds the next gap to the right' do + create_items_with_positions([2, 10]) + + new_item.update!(relative_position: 0) + expect(new_item.send(:find_next_gap_after)).to eq(start: 0, end: 2) + + new_item.update!(relative_position: 1) + expect(new_item.send(:find_next_gap_after)).to eq(start: 2, end: 10) + + new_item.update!(relative_position: 3) + expect(new_item.send(:find_next_gap_after)).to eq(start: 3, end: 10) + + new_item.update!(relative_position: 5) + expect(new_item.send(:find_next_gap_after)).to eq(start: 5, end: 10) + end + end + describe '#move_before' do + let(:item3) { create(factory, default_params) } + it 'moves item before' do - [item2, item1].each(&:move_to_end) + [item2, item1].each do |item| + item.move_to_end + item.save! + end + + expect(item1.relative_position).to be > item2.relative_position item1.move_before(item2) @@ -86,12 +337,10 @@ RSpec.shared_examples 'a class that supports relative positioning' do end context 'when there is no space' do - let(:item3) { create(factory, default_params) } - before do - item1.update(relative_position: 1000) - item2.update(relative_position: 1001) - item3.update(relative_position: 1002) + item1.update!(relative_position: 1000) + item2.update!(relative_position: 1001) + item3.update!(relative_position: 1002) end it 'moves items correctly' do @@ -100,6 +349,73 @@ RSpec.shared_examples 'a class that supports relative positioning' do expect(item3.relative_position).to be_between(item1.reload.relative_position, item2.reload.relative_position).exclusive end end + + it 'can move the item before an item at the start' do + item1.update!(relative_position: RelativePositioning::START_POSITION) + + new_item.move_before(item1) + + expect(new_item.relative_position).to be_valid_position + expect(new_item.relative_position).to be < item1.reload.relative_position + end + + it 'can move the item before an item at MIN_POSITION' do + item1.update!(relative_position: RelativePositioning::MIN_POSITION) + + new_item.move_before(item1) + + expect(new_item.relative_position).to be >= RelativePositioning::MIN_POSITION + expect(new_item.relative_position).to be < item1.reload.relative_position + end + + it 'can move the item before an item bunched up at MIN_POSITION' do + item1, item2, item3 = create_items_with_positions(run_at_start) + + new_item.move_before(item3) + new_item.save! + + items = [item1, item2, new_item, item3] + + items.each do |item| + expect(item.reset.relative_position).to be_valid_position + end + + expect(items.sort_by(&:relative_position)).to eq(items) + end + + context 'leap-frogging to the left' do + before do + start = RelativePositioning::START_POSITION + item1.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 0) + item2.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 1) + item3.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 2) + end + + let(:item3) { create(factory, default_params) } + + def leap_frog(steps) + a = item1 + b = item2 + + steps.times do |i| + a.move_before(b) + a.save! + a, b = b, a + end + end + + it 'can leap-frog STEPS - 1 times before needing to rebalance' do + # This is less efficient than going right, due to the flooring of + # integer division + expect { leap_frog(RelativePositioning::STEPS - 1) } + .not_to change { item3.reload.relative_position } + end + + it 'rebalances after leap-frogging STEPS times' do + expect { leap_frog(RelativePositioning::STEPS) } + .to change { item3.reload.relative_position } + end + end end describe '#move_after' do @@ -115,9 +431,17 @@ RSpec.shared_examples 'a class that supports relative positioning' do let(:item3) { create(factory, default_params) } before do - item1.update(relative_position: 1000) - item2.update(relative_position: 1001) - item3.update(relative_position: 1002) + item1.update!(relative_position: 1000) + item2.update!(relative_position: 1001) + item3.update!(relative_position: 1002) + end + + it 'can move the item after an item at MAX_POSITION' do + item1.update!(relative_position: RelativePositioning::MAX_POSITION) + + new_item.move_after(item1) + expect(new_item.relative_position).to be_valid_position + expect(new_item.relative_position).to be > item1.reset.relative_position end it 'moves items correctly' do @@ -126,12 +450,96 @@ RSpec.shared_examples 'a class that supports relative positioning' do expect(item1.relative_position).to be_between(item2.reload.relative_position, item3.reload.relative_position).exclusive end end + + it 'can move the item after an item bunched up at MAX_POSITION' do + item1, item2, item3 = create_items_with_positions(run_at_end) + + new_item.move_after(item1) + new_item.save! + + items = [item1, new_item, item2, item3] + + items.each do |item| + expect(item.reset.relative_position).to be_valid_position + end + + expect(items.sort_by(&:relative_position)).to eq(items) + end + + context 'leap-frogging' do + before do + start = RelativePositioning::START_POSITION + item1.update!(relative_position: start + RelativePositioning::IDEAL_DISTANCE * 0) + item2.update!(relative_position: start + RelativePositioning::IDEAL_DISTANCE * 1) + item3.update!(relative_position: start + RelativePositioning::IDEAL_DISTANCE * 2) + end + + let(:item3) { create(factory, default_params) } + + def leap_frog(steps) + a = item1 + b = item2 + + steps.times do |i| + a.move_after(b) + a.save! + a, b = b, a + end + end + + it 'can leap-frog STEPS times before needing to rebalance' do + expect { leap_frog(RelativePositioning::STEPS) } + .not_to change { item3.reload.relative_position } + end + + it 'rebalances after leap-frogging STEPS+1 times' do + expect { leap_frog(RelativePositioning::STEPS + 1) } + .to change { item3.reload.relative_position } + end + end + end + + describe '#move_to_start' do + before do + [item1, item2].each do |item1| + item1.move_to_start && item1.save! + end + end + + it 'moves item to the end' do + new_item.move_to_start + + expect(new_item.relative_position).to be < item2.relative_position + end + + it 'rebalances when there is already an item at the MIN_POSITION' do + item2.update!(relative_position: RelativePositioning::MIN_POSITION) + + new_item.move_to_start + item2.reset + + expect(new_item.relative_position).to be < item2.relative_position + expect(new_item.relative_position).to be >= RelativePositioning::MIN_POSITION + end + + it 'deals with a run of elements at the start' do + item1.update!(relative_position: RelativePositioning::MIN_POSITION + 1) + item2.update!(relative_position: RelativePositioning::MIN_POSITION) + + new_item.move_to_start + item1.reset + item2.reset + + expect(item2.relative_position).to be < item1.relative_position + expect(new_item.relative_position).to be < item2.relative_position + expect(new_item.relative_position).to be >= RelativePositioning::MIN_POSITION + end end describe '#move_to_end' do before do [item1, item2].each do |item1| - item1.move_to_end && item1.save + item1.move_to_end && item1.save! end end @@ -140,12 +548,44 @@ RSpec.shared_examples 'a class that supports relative positioning' do expect(new_item.relative_position).to be > item2.relative_position end + + it 'rebalances when there is already an item at the MAX_POSITION' do + item2.update!(relative_position: RelativePositioning::MAX_POSITION) + + new_item.move_to_end + item2.reset + + expect(new_item.relative_position).to be > item2.relative_position + expect(new_item.relative_position).to be <= RelativePositioning::MAX_POSITION + end + + it 'deals with a run of elements at the end' do + item1.update!(relative_position: RelativePositioning::MAX_POSITION - 1) + item2.update!(relative_position: RelativePositioning::MAX_POSITION) + + new_item.move_to_end + item1.reset + item2.reset + + expect(item2.relative_position).to be > item1.relative_position + expect(new_item.relative_position).to be > item2.relative_position + expect(new_item.relative_position).to be <= RelativePositioning::MAX_POSITION + end end describe '#move_between' do before do - [item1, item2].each do |item1| - item1.move_to_end && item1.save + [item1, item2].each do |item| + item.move_to_end && item.save! + end + end + + shared_examples 'moves item between' do + it 'moves the middle item to between left and right' do + expect do + middle.move_between(left, right) + middle.save! + end.to change { between_exclusive?(left, middle, right) }.from(false).to(true) end end @@ -169,26 +609,26 @@ RSpec.shared_examples 'a class that supports relative positioning' do end it 'positions items even when after and before positions are the same' do - item2.update relative_position: item1.relative_position + item2.update! relative_position: item1.relative_position new_item.move_between(item1, item2) + [item1, item2].each(&:reset) expect(new_item.relative_position).to be > item1.relative_position expect(item1.relative_position).to be < item2.relative_position end - it 'positions items between other two if distance is 1' do - item2.update relative_position: item1.relative_position + 1 - - new_item.move_between(item1, item2) + context 'the two items are next to each other' do + let(:left) { item1 } + let(:middle) { new_item } + let(:right) { create_item(relative_position: item1.relative_position + 1) } - expect(new_item.relative_position).to be > item1.relative_position - expect(item1.relative_position).to be < item2.relative_position + it_behaves_like 'moves item between' end it 'positions item in the middle of other two if distance is big enough' do - item1.update relative_position: 6000 - item2.update relative_position: 10000 + item1.update! relative_position: 6000 + item2.update! relative_position: 10000 new_item.move_between(item1, item2) @@ -196,7 +636,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do end it 'positions item closer to the middle if we are at the very top' do - item2.update relative_position: 6000 + item1.update!(relative_position: 6001) + item2.update!(relative_position: 6000) new_item.move_between(nil, item2) @@ -204,51 +645,53 @@ RSpec.shared_examples 'a class that supports relative positioning' do end it 'positions item closer to the middle if we are at the very bottom' do - new_item.update relative_position: 1 - item1.update relative_position: 6000 - item2.destroy + new_item.update!(relative_position: 1) + item1.update!(relative_position: 6000) + item2.update!(relative_position: 5999) new_item.move_between(item1, nil) expect(new_item.relative_position).to eq(6000 + RelativePositioning::IDEAL_DISTANCE) end - it 'positions item in the middle of other two if distance is not big enough' do - item1.update relative_position: 100 - item2.update relative_position: 400 + it 'positions item in the middle of other two' do + item1.update! relative_position: 100 + item2.update! relative_position: 400 new_item.move_between(item1, item2) expect(new_item.relative_position).to eq(250) end - it 'positions item in the middle of other two is there is no place' do - item1.update relative_position: 100 - item2.update relative_position: 101 + context 'there is no space' do + let(:middle) { new_item } + let(:left) { create_item(relative_position: 100) } + let(:right) { create_item(relative_position: 101) } - new_item.move_between(item1, item2) - - expect(new_item.relative_position).to be_between(item1.relative_position, item2.relative_position).exclusive + it_behaves_like 'moves item between' end - it 'uses rebalancing if there is no place' do - item1.update relative_position: 100 - item2.update relative_position: 101 - item3 = create_item(relative_position: 102) - new_item.update relative_position: 103 + context 'there is a bunch of items' do + let(:items) { create_items_with_positions(100..104) } + let(:left) { items[1] } + let(:middle) { items[3] } + let(:right) { items[2] } - new_item.move_between(item2, item3) - new_item.save! + it_behaves_like 'moves item between' + + it 'handles bunches correctly' do + middle.move_between(left, right) + middle.save! - expect(new_item.relative_position).to be_between(item2.relative_position, item3.relative_position).exclusive - expect(item1.reload.relative_position).not_to eq(100) + expect(items.first.reset.relative_position).to be < middle.relative_position + end end - it 'positions item right if we pass none-sequential parameters' do - item1.update relative_position: 99 - item2.update relative_position: 101 + it 'positions item right if we pass non-sequential parameters' do + item1.update! relative_position: 99 + item2.update! relative_position: 101 item3 = create_item(relative_position: 102) - new_item.update relative_position: 103 + new_item.update! relative_position: 103 new_item.move_between(item1, item3) new_item.save! @@ -280,6 +723,12 @@ RSpec.shared_examples 'a class that supports relative positioning' do expect(positions).to eq([90, 95, 96, 102]) end + it 'raises an error if there is no space' do + items = create_items_with_positions(run_at_start) + + expect { items.last.move_sequence_before }.to raise_error(RelativePositioning::NoSpaceLeft) + end + it 'finds a gap if there are unused positions' do items = create_items_with_positions([100, 101, 102]) @@ -287,7 +736,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do items.last.save! positions = items.map { |item| item.reload.relative_position } - expect(positions).to eq([50, 51, 102]) + + expect(positions.last - positions.second).to be > RelativePositioning::MIN_GAP end end @@ -309,7 +759,33 @@ RSpec.shared_examples 'a class that supports relative positioning' do items.first.save! positions = items.map { |item| item.reload.relative_position } - expect(positions).to eq([100, 601, 602]) + expect(positions.second - positions.first).to be > RelativePositioning::MIN_GAP end + + it 'raises an error if there is no space' do + items = create_items_with_positions(run_at_end) + + expect { items.first.move_sequence_after }.to raise_error(RelativePositioning::NoSpaceLeft) + end + end + + def be_valid_position + be_between(RelativePositioning::MIN_POSITION, RelativePositioning::MAX_POSITION) + end + + def between_exclusive?(left, middle, right) + a, b, c = [left, middle, right].map { |item| item.reset.relative_position } + return false if a.nil? || b.nil? + return a < b if c.nil? + + a < b && b < c + end + + def run_at_end(size = 3) + (RelativePositioning::MAX_POSITION - size)..RelativePositioning::MAX_POSITION + end + + def run_at_start(size = 3) + (RelativePositioning::MIN_POSITION..).take(size) end end diff --git a/spec/support/shared_examples/resource_events.rb b/spec/support/shared_examples/models/resource_event_shared_examples.rb index c0158f9b24b..c0158f9b24b 100644 --- a/spec/support/shared_examples/resource_events.rb +++ b/spec/support/shared_examples/models/resource_event_shared_examples.rb 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 new file mode 100644 index 00000000000..07552b62cdd --- /dev/null +++ b/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'timebox resource event validations' do + describe 'validations' do + context 'when issue and merge_request are both nil' do + subject { build(described_class.name.underscore.to_sym, issue: nil, merge_request: nil) } + + it { is_expected.not_to be_valid } + end + + context 'when issue and merge_request are both set' do + subject { build(described_class.name.underscore.to_sym, issue: build(:issue), merge_request: build(:merge_request)) } + + it { is_expected.not_to be_valid } + end + + context 'when issue is set' do + subject { create(described_class.name.underscore.to_sym, issue: create(:issue), merge_request: nil) } + + it { is_expected.to be_valid } + end + + context 'when merge_request is set' do + subject { create(described_class.name.underscore.to_sym, issue: nil, merge_request: create(:merge_request)) } + + it { is_expected.to be_valid } + end + end +end + +RSpec.shared_examples 'timebox resource event states' do + describe 'states' do + [Issue, MergeRequest].each do |klass| + klass.available_states.each do |state| + it "supports state #{state.first} for #{klass.name.underscore}" do + model = create(klass.name.underscore, state: state[0]) + key = model.class.name.underscore + event = build(described_class.name.underscore.to_sym, key => model, state: model.state) + + expect(event.state).to eq(state[0]) + end + end + end + end +end + +RSpec.shared_examples 'queryable timebox action resource event' do |expected_results_for_actions| + [Issue, MergeRequest].each do |klass| + expected_results_for_actions.each do |action, expected_result| + it "is #{expected_result} for action #{action} on #{klass.name.underscore}" do + model = build(klass.name.underscore) + key = model.class.name.underscore + event = build(described_class.name.underscore.to_sym, key => model, action: action) + + expect(event.send(query_method)).to eq(expected_result) + end + end + end +end + +RSpec.shared_examples 'timebox resource event actions' do + describe '#added?' do + it_behaves_like 'queryable timebox action resource event', { add: true, remove: false } do + let(:query_method) { :add? } + end + end + + describe '#removed?' do + it_behaves_like 'queryable timebox action resource event', { add: false, remove: true } do + let(:query_method) { :remove? } + end + end +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 7d70df82ec7..7f0da19996e 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 @@ -17,11 +17,14 @@ RSpec.shared_examples 'UpdateProjectStatistics' do context 'when creating' do it 'updates the project statistics' do - delta = read_attribute + delta0 = reload_stat - expect { subject.save! } - .to change { reload_stat } - .by(delta) + subject.save! + + delta1 = reload_stat + + expect(delta1).to eq(delta0 + read_attribute) + expect(delta1).to be > delta0 end it 'schedules a namespace statistics worker' do @@ -80,15 +83,14 @@ RSpec.shared_examples 'UpdateProjectStatistics' do end it 'updates the project statistics' do - delta = -read_attribute + delta0 = reload_stat - expect(ProjectStatistics) - .to receive(:increment_statistic) - .and_call_original + subject.destroy! - expect { subject.destroy! } - .to change { reload_stat } - .by(delta) + delta1 = reload_stat + + expect(delta1).to eq(delta0 - read_attribute) + expect(delta1).to be < delta0 end it 'schedules a namespace statistics worker' do diff --git a/spec/support/shared_examples/path_extraction_shared_examples.rb b/spec/support/shared_examples/path_extraction_shared_examples.rb index 19c6f2404e5..ff55bc9a490 100644 --- a/spec/support/shared_examples/path_extraction_shared_examples.rb +++ b/spec/support/shared_examples/path_extraction_shared_examples.rb @@ -88,9 +88,16 @@ RSpec.shared_examples 'extracts refs' do expect(extract_ref('stable')).to eq(['stable', '']) end - it 'extracts the longest matching ref' do - expect(extract_ref('release/app/v1.0.0/README.md')).to eq( - ['release/app/v1.0.0', 'README.md']) + it 'does not fetch ref names when there is no slash' do + expect(self).not_to receive(:ref_names) + + extract_ref('master') + end + + it 'fetches ref names when there is a slash' do + expect(self).to receive(:ref_names).and_call_original + + extract_ref('release/app/v1.0.0') end end @@ -113,6 +120,61 @@ RSpec.shared_examples 'extracts refs' do it 'falls back to a primitive split for an invalid ref' do expect(extract_ref('stable/CHANGELOG')).to eq(%w(stable CHANGELOG)) end + + it 'extracts the longest matching ref' do + expect(extract_ref('release/app/v1.0.0/README.md')).to eq( + ['release/app/v1.0.0', 'README.md']) + end + + context 'when the repository does not have ambiguous refs' do + before do + allow(container.repository).to receive(:has_ambiguous_refs?).and_return(false) + end + + it 'does not fetch all ref names when the first path component is a ref' do + expect(self).not_to receive(:ref_names) + expect(container.repository).to receive(:branch_names_include?).with('v1.0.0').and_return(false) + expect(container.repository).to receive(:tag_names_include?).with('v1.0.0').and_return(true) + + expect(extract_ref('v1.0.0/doc/README.md')).to eq(['v1.0.0', 'doc/README.md']) + end + + it 'fetches all ref names when the first path component is not a ref' do + expect(self).to receive(:ref_names).and_call_original + expect(container.repository).to receive(:branch_names_include?).with('release').and_return(false) + expect(container.repository).to receive(:tag_names_include?).with('release').and_return(false) + + expect(extract_ref('release/app/doc/README.md')).to eq(['release/app', 'doc/README.md']) + end + + context 'when the extracts_path_optimization feature flag is disabled' do + before do + stub_feature_flags(extracts_path_optimization: false) + end + + it 'always fetches all ref names' do + expect(self).to receive(:ref_names).and_call_original + expect(container.repository).not_to receive(:branch_names_include?) + expect(container.repository).not_to receive(:tag_names_include?) + + expect(extract_ref('v1.0.0/doc/README.md')).to eq(['v1.0.0', 'doc/README.md']) + end + end + end + + context 'when the repository has ambiguous refs' do + before do + allow(container.repository).to receive(:has_ambiguous_refs?).and_return(true) + end + + it 'always fetches all ref names' do + expect(self).to receive(:ref_names).and_call_original + expect(container.repository).not_to receive(:branch_names_include?) + expect(container.repository).not_to receive(:tag_names_include?) + + expect(extract_ref('v1.0.0/doc/README.md')).to eq(['v1.0.0', 'doc/README.md']) + end + end end end end diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb index df8e4bc96dd..d8476f5dcc2 100644 --- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb +++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb @@ -2,24 +2,13 @@ RSpec.shared_examples 'archived project policies' do let(:feature_write_abilities) do - described_class::READONLY_FEATURES_WHEN_ARCHIVED.flat_map do |feature| + described_class.readonly_features.flat_map do |feature| described_class.create_update_admin_destroy(feature) end + additional_maintainer_permissions end let(:other_write_abilities) do - %i[ - create_merge_request_in - create_merge_request_from - push_to_delete_protected_branch - push_code - request_access - upload_file - resolve_note - award_emoji - admin_tag - admin_issue_link - ] + described_class.readonly_abilities end context 'when the project is archived' do 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 5257980d7df..09743c20fba 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 @@ -7,13 +7,17 @@ RSpec.shared_context 'Composer user type' do |user_type, add_member| end end -RSpec.shared_examples 'Composer package index' do |user_type, status, add_member = true| +RSpec.shared_examples 'Composer package index' do |user_type, status, add_member, include_package| include_context 'Composer user type', user_type, add_member do + let(:expected_packages) { include_package == :include_package ? [package] : [] } + let(:presenter) { ::Packages::Composer::PackagesPresenter.new(group, expected_packages ) } + it 'returns the package index' do subject expect(response).to have_gitlab_http_status(status) expect(response).to match_response_schema('public_api/v4/packages/composer/index') + expect(json_response).to eq presenter.root end end end @@ -68,7 +72,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, 'register_package' + it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package' end end @@ -85,7 +89,7 @@ end RSpec.shared_context 'Composer auth headers' do |user_role, user_token| let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } end RSpec.shared_context 'Composer api project access' do |project_visibility_level, user_role, user_token| @@ -114,7 +118,7 @@ RSpec.shared_examples 'rejects Composer access with unknown group id' do end context 'as authenticated user' do - subject { get api(url), headers: build_basic_auth_header(user.username, personal_access_token.token) } + subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } it_behaves_like 'process Composer api request', :anonymous, :not_found end @@ -130,7 +134,7 @@ RSpec.shared_examples 'rejects Composer access with unknown project id' do end context 'as authenticated user' do - subject { get api(url), headers: build_basic_auth_header(user.username, personal_access_token.token) } + subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } it_behaves_like 'process Composer api request', :anonymous, :not_found end diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/subscription_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/subscription_shared_examples.rb new file mode 100644 index 00000000000..40b88ef370f --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/mutations/subscription_shared_examples.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'a subscribable resource api' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let(:project) { resource.project } + let(:input) { { subscribed_state: true } } + let(:resource_ref) { resource.class.name.camelize(:lower) } + + let(:mutation) do + variables = { + project_path: project.full_path, + iid: resource.iid.to_s + } + + graphql_mutation( + mutation_name, + variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + #{resource_ref} { + id + subscribed + } + QL + ) + end + + def mutation_response + graphql_mutation_response(mutation_name)[resource_ref]['subscribed'] + end + + context 'when the user is not authorized' do + it_behaves_like 'a mutation that returns top-level errors', + errors: ["The resource that you are attempting to access "\ + "does not exist or you don't have permission to "\ + "perform this action"] + end + + context 'when user is authorized' do + before do + project.add_developer(current_user) + end + + it 'marks the resource as subscribed' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response).to eq(true) + end + + context 'when passing subscribe false as input' do + let(:input) { { subscribed_state: false } } + + it 'unmarks the resource as subscribed' do + resource.subscribe(current_user, project) + + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response).to eq(false) + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/milestones_shared_examples.rb b/spec/support/shared_examples/requests/api/milestones_shared_examples.rb index 77b49b7caef..249a7b7cdac 100644 --- a/spec/support/shared_examples/requests/api/milestones_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/milestones_shared_examples.rb @@ -266,6 +266,7 @@ RSpec.shared_examples 'group and project milestones' do |route_definition| let!(:milestone) do context_group ? create(:milestone, group: context_group) : create(:milestone, project: public_project) end + let!(:issue) { create(:issue, project: public_project) } let!(:confidential_issue) { create(:issue, confidential: true, project: public_project) } let!(:issues_route) do diff --git a/spec/support/shared_examples/requests/api/notes_shared_examples.rb b/spec/support/shared_examples/requests/api/notes_shared_examples.rb index a34c48a5ba4..7066f803f9d 100644 --- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb @@ -158,9 +158,11 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| end it "creates an activity event when a note is created", :sidekiq_might_not_need_inline do - expect(Event).to receive(:create!) + uri = "/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes" - post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: { body: 'hi!' } + expect do + post api(uri, user), params: { body: 'hi!' } + end.to change(Event, :count).by(1) end context 'setting created_at' do @@ -275,12 +277,53 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| end describe "PUT /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id" do - it 'returns modified note' do - put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\ - "notes/#{note.id}", user), params: { body: 'Hello!' } + let(:params) { { body: 'Hello!', confidential: false } } - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['body']).to eq('Hello!') + subject do + put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{note.id}", user), params: params + end + + context 'when eveything is ok' do + before do + note.update!(confidential: true) + end + + context 'with multiple params present' do + before do + subject + end + + it 'returns modified note' do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['body']).to eq('Hello!') + expect(json_response['confidential']).to be_falsey + end + + it 'updates the note' do + expect(note.reload.note).to eq('Hello!') + expect(note.confidential).to be_falsey + end + end + + context 'when only body param is present' do + let(:params) { { body: 'Hello!' } } + + it 'updates only the note text' do + expect { subject }.not_to change { note.reload.confidential } + + expect(note.note).to eq('Hello!') + end + end + + context 'when only confidential param is present' do + let(:params) { { confidential: false } } + + it 'updates only the note text' do + expect { subject }.not_to change { note.reload.note } + + expect(note.confidential).to be_falsey + end + end end it 'returns a 404 error when note id not found' do @@ -290,9 +333,9 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| expect(response).to have_gitlab_http_status(:not_found) end - it 'returns a 400 bad request error if body not given' do + it 'returns a 400 bad request error if body is empty' do put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\ - "notes/#{note.id}", user) + "notes/#{note.id}", user), params: { body: '' } expect(response).to have_gitlab_http_status(:bad_request) 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 8d8483cae72..fcdc594f258 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 @@ -122,7 +122,7 @@ RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, sta context 'with a request that bypassed gitlab-workhorse' do let(:headers) do - build_basic_auth_header(user.username, personal_access_token.token) + basic_auth_header(user.username, personal_access_token.token) .merge(workhorse_header) .tap { |h| h.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) } end @@ -180,6 +180,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = body: 'content' ) end + let(:fog_file) { fog_to_uploaded_file(tmp_object) } let(:params) { { package: fog_file, 'package.remote_id' => file_name } } @@ -400,7 +401,7 @@ RSpec.shared_examples 'rejects nuget access with unknown project id' do end context 'as authenticated user' do - subject { get api(url), headers: build_basic_auth_header(user.username, personal_access_token.token) } + subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } it_behaves_like 'rejects nuget packages access', :anonymous, :not_found end 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 ec15d7a4d2e..6f4a0236b66 100644 --- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb @@ -2,7 +2,7 @@ RSpec.shared_examples 'deploy token for package GET requests' do context 'with deploy token headers' do - let(:headers) { build_basic_auth_header(deploy_token.username, deploy_token.token) } + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token) } subject { get api(url), headers: headers } @@ -15,7 +15,7 @@ RSpec.shared_examples 'deploy token for package GET requests' do end context 'invalid token' do - let(:headers) { build_basic_auth_header(deploy_token.username, 'bar') } + let(:headers) { basic_auth_header(deploy_token.username, 'bar') } it_behaves_like 'returning response status', :unauthorized end @@ -24,7 +24,7 @@ end RSpec.shared_examples 'deploy token for package uploads' do context 'with deploy token headers' do - let(:headers) { build_basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) } + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) } before do project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) @@ -35,7 +35,7 @@ RSpec.shared_examples 'deploy token for package uploads' do end context 'invalid token' do - let(:headers) { build_basic_auth_header(deploy_token.username, 'bar').merge(workhorse_header) } + let(:headers) { basic_auth_header(deploy_token.username, 'bar').merge(workhorse_header) } it_behaves_like 'returning response status', :unauthorized 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 fcc166ac87d..4954151b93b 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 @@ -24,6 +24,20 @@ RSpec.shared_examples 'PyPi package creation' do |user_type, status, add_member it_behaves_like 'creating pypi package files' + context 'with a pre-existing file' do + it 'rejects the duplicated file' do + existing_package = create(:pypi_package, name: base_params[:name], version: base_params[:version], project: project) + create(:package_file, :pypi, package: existing_package, file_name: params[:content].original_filename) + + expect { subject } + .to change { project.packages.pypi.count }.by(0) + .and change { Packages::PackageFile.count }.by(0) + .and change { Packages::Pypi::Metadatum.count }.by(0) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + context 'with object storage disabled' do before do stub_package_file_object_storage(enabled: false) @@ -49,6 +63,7 @@ RSpec.shared_examples 'PyPi package creation' do |user_type, status, add_member body: 'content' ) end + let(:fog_file) { fog_to_uploaded_file(tmp_object) } let(:params) { base_params.merge(content: fog_file, 'content.remote_id' => file_name) } @@ -144,7 +159,7 @@ RSpec.shared_examples 'rejects PyPI access with unknown project id' do end context 'as authenticated user' do - subject { get api(url), headers: build_basic_auth_header(user.username, personal_access_token.token) } + subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } it_behaves_like 'process PyPi api request', :anonymous, :not_found end diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb index 644abb191a6..a17163328f4 100644 --- a/spec/support/shared_examples/requests/snippet_shared_examples.rb +++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb @@ -106,3 +106,80 @@ RSpec.shared_examples 'snippet_multiple_files feature disabled' do expect(json_response).not_to have_key('files') end end + +RSpec.shared_examples 'snippet creation with files parameter' do + using RSpec::Parameterized::TableSyntax + + where(:path, :content, :status, :error) do + '.gitattributes' | 'file content' | :created | nil + 'valid/path/file.rb' | 'file content' | :created | nil + + '.gitattributes' | nil | :bad_request | 'files[0][content] is empty' + '.gitattributes' | '' | :bad_request | 'files[0][content] is empty' + + '' | 'file content' | :bad_request | 'files[0][file_path] is empty' + nil | 'file content' | :bad_request | 'files[0][file_path] should be a valid file path, files[0][file_path] is empty' + '../../etc/passwd' | 'file content' | :bad_request | 'files[0][file_path] should be a valid file path' + end + + with_them do + let(:file_path) { path } + let(:file_content) { content } + + before do + subject + end + + it 'responds correctly' do + expect(response).to have_gitlab_http_status(status) + expect(json_response['error']).to eq(error) + end + end + + it 'returns 400 if both files and content are provided' do + params[:file_name] = 'foo.rb' + params[:content] = 'bar' + + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq 'files, content are mutually exclusive' + end + + it 'returns 400 when neither files or content are provided' do + params.delete(:files) + + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq 'files, content are missing, exactly one parameter must be provided' + end +end + +RSpec.shared_examples 'snippet creation without files parameter' do + let(:file_params) { { file_name: 'testing.rb', content: 'snippet content' } } + + it 'allows file_name and content parameters' do + subject + + expect(response).to have_gitlab_http_status(:created) + end + + it 'returns 400 if file_name and content are not both provided' do + params.delete(:file_name) + + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq 'file_name is missing' + end + + it 'returns 400 if content is blank' do + params[:content] = '' + + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq 'content is empty' + end +end diff --git a/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb index 756c4136059..06e2b715e6d 100644 --- a/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb @@ -19,6 +19,15 @@ RSpec.shared_examples 'issues list service' do end end + it 'avoids N+1' do + params = { board_id: board.id } + control = ActiveRecord::QueryRecorder.new { described_class.new(parent, user, params).execute } + + create(:list, board: board) + + expect { described_class.new(parent, user, params).execute }.not_to exceed_query_limit(control) + end + context 'issues are ordered by priority' do it 'returns opened issues when list_id is missing' do params = { board_id: board.id } @@ -71,4 +80,17 @@ RSpec.shared_examples 'issues list service' do expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound) end end + + context 'when :all_lists is used' do + it 'returns issues from all lists' do + params = { board_id: board.id, all_lists: true } + + issues = described_class.new(parent, user, params).execute + + expected = [opened_issue2, reopened_issue1, opened_issue1, list1_issue1, + list1_issue2, list1_issue3, list2_issue1, closed_issue1, + closed_issue2, closed_issue3, closed_issue4, closed_issue5] + expect(issues).to match_array(expected) + end + end end diff --git a/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb index 07a6353296d..41fd286682e 100644 --- a/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb @@ -26,4 +26,22 @@ RSpec.shared_examples 'lists list service' do expect(service.execute(board)).to eq [board.backlog_list, list, board.closed_list] end end + + context 'when wanting a specific list' do + let!(:list1) { create(:list, board: board) } + + it 'returns list specified by id' do + service = described_class.new(parent, user, list_id: list1.id) + + expect(service.execute(board, create_default_lists: false)).to eq [list1] + end + + it 'returns empty result when list is not found' do + external_board = create(:board, resource_parent: create(:project)) + external_list = create(:list, board: external_board) + service = described_class.new(parent, user, list_id: external_list.id) + + expect(service.execute(board, create_default_lists: false)).to eq(List.none) + end + end end diff --git a/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb b/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb new file mode 100644 index 00000000000..7fc7ff8a8de --- /dev/null +++ b/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'mapping jira users' do + let(:client) { double } + + let_it_be(:project) { create(:project) } + let_it_be(:jira_service) { create(:jira_service, project: project, active: true) } + + before do + allow(subject).to receive(:client).and_return(client) + allow(client).to receive(:get).with(url).and_return(jira_users) + end + + subject { described_class.new(jira_service, start_at) } + + context 'jira_users is nil' do + let(:jira_users) { nil } + + it 'returns an empty array' do + expect(subject.execute).to be_empty + end + end + + context 'when jira_users is present' do + # TODO: now we only create an array in a proper format + # mapping is tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/219023 + let(:mapped_users) do + [ + { jira_account_id: 'abcd', jira_display_name: 'user1', jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }, + { jira_account_id: 'efg', jira_display_name: nil, jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }, + { jira_account_id: 'hij', jira_display_name: 'user3', jira_email: 'user3@example.com', gitlab_id: nil, gitlab_username: nil, gitlab_name: nil } + ] + end + + it 'returns users mapped to Gitlab' do + expect(subject.execute).to eq(mapped_users) + end + end +end diff --git a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb index c8fabfe30b9..1501a2a0f52 100644 --- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb +++ b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb @@ -62,7 +62,7 @@ end RSpec.shared_examples 'dashboard_version contains SHA256 hash of dashboard file content' do specify do dashboard = File.read(Rails.root.join(dashboard_path)) - expect(Digest::SHA256.hexdigest(dashboard)).to eq(dashboard_version) + expect(dashboard_version).to eq(Digest::SHA256.hexdigest(dashboard)) end end @@ -78,6 +78,12 @@ RSpec.shared_examples 'raises error for users with insufficient permissions' do it_behaves_like 'misconfigured dashboard service response', :unauthorized end + + context 'when the user is anonymous' do + let(:user) { nil } + + it_behaves_like 'misconfigured dashboard service response', :unauthorized + end end RSpec.shared_examples 'valid dashboard cloning process' do |dashboard_template, sequence| diff --git a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb index 2ddbdebdb97..f201c7b1780 100644 --- a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb +++ b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb @@ -2,9 +2,11 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| let(:project_repository_double) { double(:repository) } + let(:original_project_repository_double) { double(:repository) } let!(:project_repository_checksum) { project.repository.checksum } let(:repository_double) { double(:repository) } + let(:original_repository_double) { double(:repository) } let(:repository_checksum) { repository.checksum } before do @@ -14,10 +16,16 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| allow(Gitlab::Git::Repository).to receive(:new) .with('test_second_storage', project.repository.raw.relative_path, project.repository.gl_repository, project.repository.full_path) .and_return(project_repository_double) + allow(Gitlab::Git::Repository).to receive(:new) + .with('default', project.repository.raw.relative_path, nil, nil) + .and_return(original_project_repository_double) allow(Gitlab::Git::Repository).to receive(:new) .with('test_second_storage', repository.raw.relative_path, repository.gl_repository, repository.full_path) .and_return(repository_double) + allow(Gitlab::Git::Repository).to receive(:new) + .with('default', repository.raw.relative_path, nil, nil) + .and_return(original_repository_double) end context 'when the move succeeds', :clean_gitlab_redis_shared_state do @@ -35,8 +43,8 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| allow(repository_double).to receive(:checksum) .and_return(repository_checksum) - expect(GitlabShellWorker).to receive(:perform_async).with(:mv_repository, 'default', anything, anything) - .twice.and_call_original + expect(original_project_repository_double).to receive(:remove) + expect(original_repository_double).to receive(:remove) end it "moves the project and its #{repository_type} repository to the new storage and unmarks the repository as read only" do @@ -110,13 +118,36 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| .with(repository.raw) .and_raise(Gitlab::Git::CommandError) - expect(GitlabShellWorker).not_to receive(:perform_async) - result = subject.execute expect(result).to be_error expect(project).not_to be_repository_read_only expect(project.repository_storage).to eq('default') + expect(repository_storage_move).to be_failed + end + end + + context "when the cleanup of the #{repository_type} repository fails" do + it 'sets the correct state' do + allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('default').and_call_original + allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('test_second_storage').and_return(SecureRandom.uuid) + allow(project_repository_double).to receive(:replicate) + .with(project.repository.raw) + allow(project_repository_double).to receive(:checksum) + .and_return(project_repository_checksum) + allow(original_project_repository_double).to receive(:remove) + allow(repository_double).to receive(:replicate) + .with(repository.raw) + allow(repository_double).to receive(:checksum) + .and_return(repository_checksum) + + expect(original_repository_double).to receive(:remove) + .and_raise(Gitlab::Git::CommandError) + + result = subject.execute + + expect(result).to be_error + expect(repository_storage_move).to be_cleanup_failed end end @@ -134,8 +165,6 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| allow(repository_double).to receive(:checksum) .and_return('not matching checksum') - expect(GitlabShellWorker).not_to receive(:perform_async) - result = subject.execute expect(result).to be_error diff --git a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb index ef41c2fcc13..d70ed707822 100644 --- a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb +++ b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb @@ -1,49 +1,63 @@ # frozen_string_literal: true -RSpec.shared_examples 'a milestone events creator' do +RSpec.shared_examples 'timebox(milestone or iteration) resource events creator' do |timebox_event_class| let_it_be(:user) { create(:user) } - let(:created_at_time) { Time.utc(2019, 12, 30) } - let(:service) { described_class.new(resource, user, created_at: created_at_time, old_milestone: nil) } - - context 'when milestone is present' do - let_it_be(:milestone) { create(:milestone) } + context 'when milestone/iteration is added' do + let(:service) { described_class.new(resource, user, add_timebox_args) } before do - resource.milestone = milestone + set_timebox(timebox_event_class, timebox) end it 'creates the expected event record' do - expect { service.execute }.to change { ResourceMilestoneEvent.count }.by(1) + expect { service.execute }.to change { timebox_event_class.count }.by(1) - expect_event_record(ResourceMilestoneEvent.last, action: 'add', milestone: milestone, state: 'opened') + expect_event_record(timebox_event_class, timebox_event_class.last, action: 'add', state: 'opened', timebox: timebox) end end - context 'when milestones is not present' do + context 'when milestone/iteration is removed' do + let(:service) { described_class.new(resource, user, remove_timebox_args) } + before do - resource.milestone = nil + set_timebox(timebox_event_class, nil) end - let(:old_milestone) { create(:milestone, project: resource.project) } - let(:service) { described_class.new(resource, user, created_at: created_at_time, old_milestone: old_milestone) } - it 'creates the expected event records' do - expect { service.execute }.to change { ResourceMilestoneEvent.count }.by(1) + expect { service.execute }.to change { timebox_event_class.count }.by(1) - expect_event_record(ResourceMilestoneEvent.last, action: 'remove', milestone: old_milestone, state: 'opened') + expect_event_record(timebox_event_class, timebox_event_class.last, action: 'remove', timebox: timebox, state: 'opened') end end - def expect_event_record(event, expected_attrs) + def expect_event_record(timebox_event_class, event, expected_attrs) expect(event.action).to eq(expected_attrs[:action]) - expect(event.state).to eq(expected_attrs[:state]) expect(event.user).to eq(user) expect(event.issue).to eq(resource) if resource.is_a?(Issue) expect(event.issue).to be_nil unless resource.is_a?(Issue) expect(event.merge_request).to eq(resource) if resource.is_a?(MergeRequest) expect(event.merge_request).to be_nil unless resource.is_a?(MergeRequest) - expect(event.milestone).to eq(expected_attrs[:milestone]) expect(event.created_at).to eq(created_at_time) + expect_timebox(timebox_event_class, event, expected_attrs) + end + + def set_timebox(timebox_event_class, timebox) + case timebox_event_class.name + when 'ResourceMilestoneEvent' + resource.milestone = timebox + when 'ResourceIterationEvent' + resource.iteration = timebox + end + end + + def expect_timebox(timebox_event_class, event, expected_attrs) + case timebox_event_class.name + when 'ResourceMilestoneEvent' + expect(event.state).to eq(expected_attrs[:state]) + expect(event.milestone).to eq(expected_attrs[:timebox]) + when 'ResourceIterationEvent' + expect(event.iteration).to eq(expected_attrs[:timebox]) + end end end diff --git a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb index ebe78c299a5..980a752cf86 100644 --- a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb +++ b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb @@ -16,8 +16,10 @@ RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type| subject(:service) { described_class.new(container: container, current_user: user, params: opts) } it 'creates wiki page with valid attributes' do - page = service.execute + response = service.execute + page = response.payload[:page] + expect(response).to be_success expect(page).to be_valid expect(page).to be_persisted expect(page.title).to eq(opts[:title]) @@ -77,7 +79,12 @@ RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type| end it 'reports the error' do - expect(service.execute).to be_invalid + response = service.execute + page = response.payload[:page] + + expect(response).to be_error + + expect(page).to be_invalid .and have_attributes(errors: be_present) end end diff --git a/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb b/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb index 541e332e3a1..555a6d5eed0 100644 --- a/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb +++ b/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb @@ -14,6 +14,7 @@ RSpec.shared_examples 'Wikis::CreateAttachmentService#execute' do |container_typ file_content: 'Content of attachment' } end + let(:opts) { file_opts } let(:service) { Wikis::CreateAttachmentService.new(container: container, current_user: user, params: opts) } diff --git a/spec/support/shared_examples/snippet_blob_shared_examples.rb b/spec/support/shared_examples/snippet_blob_shared_examples.rb index ba97688d017..3ed777ee4b8 100644 --- a/spec/support/shared_examples/snippet_blob_shared_examples.rb +++ b/spec/support/shared_examples/snippet_blob_shared_examples.rb @@ -22,3 +22,24 @@ RSpec.shared_examples 'snippet blob raw path' do end end end + +RSpec.shared_examples 'snippet blob raw url' do + let(:blob) { snippet.blobs.first } + let(:ref) { blob.repository.root_ref } + + context 'for PersonalSnippets' do + let(:snippet) { personal_snippet } + + it 'returns the raw personal snippet blob url' do + expect(subject).to eq("http://test.host/-/snippets/#{snippet.id}/raw/#{ref}/#{blob.path}") + end + end + + context 'for ProjectSnippets' do + let(:snippet) { project_snippet } + + it 'returns the raw project snippet blob url' do + expect(subject).to eq("http://test.host/#{snippet.project.full_path}/-/snippets/#{snippet.id}/raw/#{ref}/#{blob.path}") + end + end +end |