summaryrefslogtreecommitdiff
path: root/spec/support/shared_examples
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-08-20 18:42:06 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-08-20 18:42:06 +0000
commit6e4e1050d9dba2b7b2523fdd1768823ab85feef4 (patch)
tree78be5963ec075d80116a932011d695dd33910b4e /spec/support/shared_examples
parent1ce776de4ae122aba3f349c02c17cebeaa8ecf07 (diff)
downloadgitlab-ce-6e4e1050d9dba2b7b2523fdd1768823ab85feef4.tar.gz
Add latest changes from gitlab-org/gitlab@13-3-stable-ee
Diffstat (limited to 'spec/support/shared_examples')
-rw-r--r--spec/support/shared_examples/alert_notification_service_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/controllers/binary_blob_shared_examples.rb34
-rw-r--r--spec/support/shared_examples/controllers/concerns/graceful_timeout_handling_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb83
-rw-r--r--spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/controllers/variables_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb54
-rw-r--r--spec/support/shared_examples/create_alert_issue_shared_examples.rb27
-rw-r--r--spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb21
-rw-r--r--spec/support/shared_examples/features/packages_shared_examples.rb113
-rw-r--r--spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/features/rss_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/snippets_shared_examples.rb222
-rw-r--r--spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb386
-rw-r--r--spec/support/shared_examples/graphql/design_fields_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/graphql/mutations/resolves_subscription_shared_examples.rb45
-rw-r--r--spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb126
-rw-r--r--spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb11
-rw-r--r--spec/support/shared_examples/lib/api/ci/runner_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb160
-rw-r--r--spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb49
-rw-r--r--spec/support/shared_examples/models/chat_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb38
-rw-r--r--spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/models/cluster_application_status_shared_examples.rb152
-rw-r--r--spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb176
-rw-r--r--spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/models/concerns/timebox_shared_examples.rb41
-rw-r--r--spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/models/relative_positioning_shared_examples.rb602
-rw-r--r--spec/support/shared_examples/models/resource_event_shared_examples.rb (renamed from spec/support/shared_examples/resource_events.rb)0
-rw-r--r--spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb75
-rw-r--r--spec/support/shared_examples/models/update_project_statistics_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/path_extraction_shared_examples.rb68
-rw-r--r--spec/support/shared_examples/policies/project_policy_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/requests/api/graphql/mutations/subscription_shared_examples.rb69
-rw-r--r--spec/support/shared_examples/requests/api/milestones_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/requests/api/notes_shared_examples.rb61
-rw-r--r--spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/requests/api/packages_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/requests/snippet_shared_examples.rb77
-rw-r--r--spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb39
-rw-r--r--spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb41
-rw-r--r--spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb52
-rw-r--r--spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/snippet_blob_shared_examples.rb21
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