diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-20 15:26:25 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-20 15:26:25 +0300 |
commit | a09983ae35713f5a2bbb100981116d31ce99826e (patch) | |
tree | 2ee2af7bd104d57086db360a7e6d8c9d5d43667a /spec/support/shared_examples | |
parent | 18c5ab32b738c0b6ecb4d0df3994000482f34bd8 (diff) |
Add latest changes from gitlab-org/gitlab@13-2-stable-ee
Diffstat (limited to 'spec/support/shared_examples')
70 files changed, 2646 insertions, 365 deletions
diff --git a/spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb deleted file mode 100644 index 88ad1f6cde2..00000000000 --- a/spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'import controller with new_import_ui feature flag' do - include ImportSpecHelper - - context 'with new_import_ui feature flag enabled' do - let(:group) { create(:group) } - - before do - stub_feature_flags(new_import_ui: true) - group.add_owner(user) - end - - it "returns variables for json request" do - project = create(:project, import_type: provider_name, creator_id: user.id) - stub_client(client_repos_field => [repo]) - - get :status, format: :json - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) - expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id) - expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) - end - - it "does not show already added project" do - project = create(:project, import_type: provider_name, namespace: user.namespace, import_status: :finished, import_source: import_source) - stub_client(client_repos_field => [repo]) - - get :status, format: :json - - expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) - expect(json_response.dig("provider_repos")).to eq([]) - end - end -end diff --git a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb new file mode 100644 index 00000000000..ecb9abc5c46 --- /dev/null +++ b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'import controller status' do + include ImportSpecHelper + + let(:group) { create(:group) } + + before do + group.add_owner(user) + end + + it "returns variables for json request" do + project = create(:project, import_type: provider_name, creator_id: user.id) + stub_client(client_repos_field => [repo]) + + get :status, format: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) + expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id) + expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) + end + + it "does not show already added project" do + project = create(:project, import_type: provider_name, namespace: user.namespace, import_status: :finished, import_source: import_source) + stub_client(client_repos_field => [repo]) + + get :status, format: :json + + expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) + expect(json_response.dig("provider_repos")).to eq([]) + end +end diff --git a/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb index 60abb76acec..7f26155f9d6 100644 --- a/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb +++ b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb @@ -9,13 +9,38 @@ RSpec.shared_examples 'known sign in' do user.update!(current_sign_in_ip: ip) end - context 'with a valid post' do - context 'when remote IP does not match user last sign in IP' do - before do - stub_user_ip('127.0.0.1') - stub_remote_ip('169.0.0.1') - end + def stub_cookie(value = user.id) + cookies.encrypted[KnownSignIn::KNOWN_SIGN_IN_COOKIE] = { + value: value, expires: KnownSignIn::KNOWN_SIGN_IN_COOKIE_EXPIRY + } + end + + context 'when the remote IP and the last sign in IP match' do + before do + stub_user_ip('169.0.0.1') + stub_remote_ip('169.0.0.1') + end + + it 'does not notify the user' do + expect(NotificationService).not_to receive(:new) + post_action + end + + it 'sets/updates the encrypted cookie' do + post_action + + expect(cookies.encrypted[KnownSignIn::KNOWN_SIGN_IN_COOKIE]).to eq(user.id) + end + end + + context 'when the remote IP and the last sign in IP do not match' do + before do + stub_user_ip('127.0.0.1') + stub_remote_ip('169.0.0.1') + end + + context 'when the cookie is not previously set' do it 'notifies the user' do expect_next_instance_of(NotificationService) do |instance| expect(instance).to receive(:unknown_sign_in) @@ -23,37 +48,68 @@ RSpec.shared_examples 'known sign in' do post_action end - end - - context 'when remote IP matches an active session' do - before do - existing_sessions = ActiveSession.session_ids_for_user(user.id) - existing_sessions.each { |sessions| ActiveSession.destroy(user, sessions) } - stub_user_ip('169.0.0.1') - stub_remote_ip('127.0.0.1') + it 'sets the encrypted cookie' do + post_action - ActiveSession.set(user, request) + expect(cookies.encrypted[KnownSignIn::KNOWN_SIGN_IN_COOKIE]).to eq(user.id) end + end - it 'does not notify the user' do - expect_any_instance_of(NotificationService).not_to receive(:unknown_sign_in) + it 'notifies the user when the cookie is expired' do + stub_cookie + + Timecop.freeze((KnownSignIn::KNOWN_SIGN_IN_COOKIE_EXPIRY + 1.day).from_now) do + expect_next_instance_of(NotificationService) do |instance| + expect(instance).to receive(:unknown_sign_in) + end post_action end end - context 'when remote IP address matches last sign in IP' do + context 'when notify_on_unknown_sign_in global setting is false' do before do - stub_user_ip('127.0.0.1') - stub_remote_ip('127.0.0.1') + stub_application_setting(notify_on_unknown_sign_in: false) end it 'does not notify the user' do - expect_any_instance_of(NotificationService).not_to receive(:unknown_sign_in) + expect(NotificationService).not_to receive(:new) + + post_action + end + it 'does not set a cookie' do post_action + + expect(cookies.encrypted[KnownSignIn::KNOWN_SIGN_IN_COOKIE]).to be_nil + end + end + + it 'notifies the user when the cookie is for another user' do + stub_cookie(create(:user).id) + + expect_next_instance_of(NotificationService) do |instance| + expect(instance).to receive(:unknown_sign_in) end + + post_action + end + + it 'does not notify the user when remote IP matches an active session' do + ActiveSession.set(user, request) + + expect(NotificationService).not_to receive(:new) + + post_action + end + + it 'does not notify the user when the cookie is present and not expired' do + stub_cookie + + expect(NotificationService).not_to receive(:new) + + post_action end end 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 new file mode 100644 index 00000000000..94cd6971f7c --- /dev/null +++ b/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'metrics dashboard prometheus api proxy' do + let(:service_params) { [proxyable, 'GET', 'query', expected_params] } + let(:service_result) { { status: :success, body: prometheus_body } } + let(:prometheus_proxy_service) { instance_double(Prometheus::ProxyService) } + let(:proxyable_params) do + { + id: proxyable.id.to_s + } + end + let(:expected_params) do + ActionController::Parameters.new( + prometheus_proxy_params( + proxy_path: 'query', + controller: described_class.controller_path, + action: 'prometheus_proxy' + ) + ).permit! + end + + before do + allow_next_instance_of(Prometheus::ProxyService, *service_params) do |proxy_service| + allow(proxy_service).to receive(:execute).and_return(service_result) + end + end + + context 'with valid requests' do + context 'with success result' do + let(:prometheus_body) { '{"status":"success"}' } + let(:prometheus_json_body) { Gitlab::Json.parse(prometheus_body) } + + it 'returns prometheus response' do + get :prometheus_proxy, params: prometheus_proxy_params + + expect(Prometheus::ProxyService).to have_received(:new).with(*service_params) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq(prometheus_json_body) + end + + context 'with nil query' do + let(:params_without_query) do + prometheus_proxy_params.except(:query) + end + + before do + expected_params.delete(:query) + end + + it 'does not raise error' do + get :prometheus_proxy, params: params_without_query + + expect(Prometheus::ProxyService).to have_received(:new).with(*service_params) + end + end + end + + context 'with nil result' do + let(:service_result) { nil } + + it 'returns 204 no_content' do + get :prometheus_proxy, params: prometheus_proxy_params + + expect(json_response['status']).to eq(_('processing')) + expect(json_response['message']).to eq(_('Not ready yet. Try again later.')) + expect(response).to have_gitlab_http_status(:no_content) + end + end + + context 'with 404 result' do + let(:service_result) { { http_status: 404, status: :success, body: '{"body": "value"}' } } + + it 'returns body' do + get :prometheus_proxy, params: prometheus_proxy_params + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['body']).to eq('value') + end + end + + context 'with error result' do + context 'with http_status' do + let(:service_result) do + { http_status: :service_unavailable, status: :error, message: 'error message' } + end + + it 'sets the http response status code' do + get :prometheus_proxy, params: prometheus_proxy_params + + expect(response).to have_gitlab_http_status(:service_unavailable) + expect(json_response['status']).to eq('error') + expect(json_response['message']).to eq('error message') + end + end + + context 'without http_status' do + let(:service_result) { { status: :error, message: 'error message' } } + + it 'returns bad_request' do + get :prometheus_proxy, params: prometheus_proxy_params + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['status']).to eq('error') + expect(json_response['message']).to eq('error message') + end + end + end + end + + context 'with inappropriate requests' do + let(:prometheus_body) { nil } + + context 'without correct permissions' do + let(:user2) { create(:user) } + + before do + sign_out(user) + sign_in(user2) + end + + it 'returns 404' do + get :prometheus_proxy, params: prometheus_proxy_params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'with invalid proxyable id' do + let(:prometheus_body) { nil } + + it 'returns 404' do + get :prometheus_proxy, params: prometheus_proxy_params(id: proxyable.id + 1) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + private + + def prometheus_proxy_params(params = {}) + { + proxy_path: 'query', + query: '1' + }.merge(proxyable_params).merge(params) + end +end diff --git a/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb b/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb new file mode 100644 index 00000000000..cb8f6721d66 --- /dev/null +++ b/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'GET #metrics_dashboard correctly formatted response' do + it 'returns a json object with the correct keys' do + get :metrics_dashboard, params: metrics_dashboard_req_params, format: :json + + # Exclude `all_dashboards` to handle separately, at spec/controllers/projects/environments_controller_spec.rb:565 + # because `all_dashboards` key is not part of expected shared behavior + found_keys = json_response.keys - ['all_dashboards'] + + expect(response).to have_gitlab_http_status(status_code) + expect(found_keys).to contain_exactly(*expected_keys) + end +end + +RSpec.shared_examples_for 'GET #metrics_dashboard for dashboard' do |dashboard_name| + let(:expected_keys) { %w(dashboard status metrics_data) } + let(:status_code) { :ok } + + it_behaves_like 'GET #metrics_dashboard correctly formatted response' + + it 'returns correct dashboard' do + get :metrics_dashboard, params: metrics_dashboard_req_params, format: :json + + expect(json_response['dashboard']['dashboard']).to eq(dashboard_name) + end +end diff --git a/spec/support/shared_examples/controllers/namespace_storage_limit_alert_shared_examples.rb b/spec/support/shared_examples/controllers/namespace_storage_limit_alert_shared_examples.rb deleted file mode 100644 index 7885eb6c1f8..00000000000 --- a/spec/support/shared_examples/controllers/namespace_storage_limit_alert_shared_examples.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'namespace storage limit alert' do - let(:alert_level) { :info } - - before do - allow_next_instance_of(Namespaces::CheckStorageSizeService, namespace, user) do |check_storage_size_service| - expect(check_storage_size_service).to receive(:execute).and_return( - ServiceResponse.success( - payload: { - alert_level: alert_level, - usage_message: "Usage", - explanation_message: "Explanation", - root_namespace: namespace - } - ) - ) - end - - allow(controller).to receive(:current_user).and_return(user) - end - - render_views - - it 'does render' do - subject - - expect(response.body).to match(/Explanation/) - expect(response.body).to have_css('.js-namespace-storage-alert-dismiss') - end - - context 'when alert_level is error' do - let(:alert_level) { :error } - - it 'does not render a dismiss button' do - subject - - expect(response.body).not_to have_css('.js-namespace-storage-alert-dismiss') - end - end - - context 'when cookie is set' do - before do - cookies["hide_storage_limit_alert_#{namespace.id}_info"] = 'true' - end - - it 'does not render alert' do - subject - - expect(response.body).not_to match(/Explanation/) - end - end -end diff --git a/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb b/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb new file mode 100644 index 00000000000..c3e8f807afb --- /dev/null +++ b/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'raw snippet blob' do + context 'with valid params' do + before do + subject + end + + it 'delivers file with correct Workhorse headers' do + expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') + expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true' + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') + end + + it 'responds with status 200' do + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'with invalid file path' do + let(:filepath) { 'doesnotexist' } + + it_behaves_like 'returning response status', :not_found + end + + context 'with invalid ref' do + let(:ref) { 'doesnotexist' } + + it_behaves_like 'returning response status', :not_found + end + + it_behaves_like 'content disposition headers' +end + +RSpec.shared_examples 'raw snippet without repository' do |unauthorized_status| + context 'when authorized' do + it 'returns a 422' do + subject + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end + + context 'when unauthorized' do + let(:visibility) { :private } + + it_behaves_like 'returning response status', unauthorized_status + end +end diff --git a/spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb b/spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb new file mode 100644 index 00000000000..aa4d78b23f4 --- /dev/null +++ b/spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'snippets sort order' do + let(:params) { {} } + let(:sort_argument) { {} } + let(:sort_params) { params.merge(sort_argument)} + + before do + sign_in(user) + + stub_snippet_counter + end + + subject { get :index, params: sort_params } + + context 'when no sort param is provided' do + it 'calls SnippetsFinder with updated_at sort option' do + expect(SnippetsFinder).to receive(:new).with(user, + hash_including(sort: 'updated_desc')).and_call_original + + subject + end + end + + context 'when sort param is provided' do + let(:order) { 'created_desc' } + let(:sort_argument) { { sort: order } } + + it 'calls SnippetsFinder with the given sort param' do + expect(SnippetsFinder).to receive(:new).with(user, + hash_including(sort: order)).and_call_original + + subject + end + end + + def stub_snippet_counter + allow(Snippets::CountService) + .to receive(:new).and_return(double(:count_service, execute: {})) + end +end diff --git a/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb b/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb new file mode 100644 index 00000000000..90588756eb0 --- /dev/null +++ b/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'tracking unique visits' do |method| + it 'tracks unique visit if the format is HTML' do + expect_any_instance_of(Gitlab::Analytics::UniqueVisits).to receive(:track_visit).with(instance_of(String), target_id) + + get method, params: request_params, format: :html + end + + it 'tracks unique visit if DNT is not enabled' do + expect_any_instance_of(Gitlab::Analytics::UniqueVisits).to receive(:track_visit).with(instance_of(String), target_id) + request.headers['DNT'] = '0' + + get method, params: request_params, format: :html + end + + it 'does not track unique visit if DNT is enabled' do + expect_any_instance_of(Gitlab::Analytics::UniqueVisits).not_to receive(:track_visit) + request.headers['DNT'] = '1' + + get method, params: request_params, format: :html + end + + it 'does not track unique visit if the format is JSON' do + expect_any_instance_of(Gitlab::Analytics::UniqueVisits).not_to receive(:track_visit) + + get method, params: request_params, format: :json + end +end diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb index b5f2c0d07bf..4df3139d56e 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -104,6 +104,35 @@ RSpec.shared_examples 'wiki controller actions' do end end + describe 'GET #diff' do + context 'when commit exists' do + it 'renders the diff' do + get :diff, params: routing_params.merge(id: wiki_title, version_id: wiki.repository.commit.id) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('shared/wikis/diff') + expect(assigns(:diffs)).to be_a(Gitlab::Diff::FileCollection::Base) + expect(assigns(:diff_notes_disabled)).to be(true) + end + end + + context 'when commit does not exist' do + it 'returns a 404 error' do + get :diff, params: routing_params.merge(id: wiki_title, version_id: 'invalid') + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when page does not exist' do + it 'returns a 404 error' do + get :diff, params: routing_params.merge(id: 'invalid') + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + describe 'GET #show' do render_views @@ -118,6 +147,7 @@ RSpec.shared_examples 'wiki controller actions' do subject expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('shared/wikis/show') expect(assigns(:page).title).to eq(wiki_title) expect(assigns(:sidebar_wiki_entries)).to contain_exactly(an_instance_of(WikiPage)) expect(assigns(:sidebar_limited)).to be(false) @@ -130,6 +160,7 @@ RSpec.shared_examples 'wiki controller actions' do subject expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('shared/wikis/show') expect(flash[:notice]).to eq(_('The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.')) end end @@ -138,19 +169,37 @@ RSpec.shared_examples 'wiki controller actions' do context 'when the page does not exist' do let(:id) { 'does not exist' } - before do - subject - end + context 'when the user can create pages' do + before do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('shared/wikis/edit') + end + + it 'builds a new wiki page with the id as the title' do + expect(assigns(:page).title).to eq(id) + end + + context 'when a random_title param is present' do + let(:random_title) { true } - it 'builds a new wiki page with the id as the title' do - expect(assigns(:page).title).to eq(id) + it 'builds a new wiki page with no title' do + expect(assigns(:page).title).to be_empty + end + end end - context 'when a random_title param is present' do - let(:random_title) { true } + context 'when the user cannot create pages' do + before do + sign_out(:user) + end - it 'builds a new wiki page with no title' do - expect(assigns(:page).title).to be_empty + it 'shows the empty state' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('shared/wikis/empty') end end end @@ -166,6 +215,7 @@ RSpec.shared_examples 'wiki controller actions' do it 'delivers the file with the correct headers' do subject + expect(response).to have_gitlab_http_status(:ok) expect(response.headers['Content-Disposition']).to match(/^inline/) expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq('true') expect(response.cache_control[:public]).to be(false) @@ -179,12 +229,31 @@ RSpec.shared_examples 'wiki controller actions' do it 'renders json in a correct format' do post :preview_markdown, params: routing_params.merge(id: 'page/path', text: '*Markdown* text') + expect(response).to have_gitlab_http_status(:ok) expect(json_response.keys).to match_array(%w(body references)) end end - describe 'GET #edit' do - subject { get(:edit, params: routing_params.merge(id: wiki_title)) } + shared_examples 'edit action' do + context 'when the page does not exist' do + let(:id_param) { 'invalid' } + + it 'redirects to show' do + subject + + expect(response).to redirect_to_wiki(wiki, 'invalid') + end + end + + context 'when id param is blank' do + let(:id_param) { ' ' } + + it 'redirects to the home page' do + subject + + expect(response).to redirect_to_wiki(wiki, 'home') + end + end context 'when page content encoding is invalid' do it 'redirects to show' do @@ -208,6 +277,14 @@ RSpec.shared_examples 'wiki controller actions' do expect(response).to redirect_to_wiki(wiki, page) end end + end + + describe 'GET #edit' do + let(:id_param) { wiki_title } + + subject { get(:edit, params: routing_params.merge(id: id_param)) } + + it_behaves_like 'edit action' context 'when page content encoding is valid' do render_views @@ -224,23 +301,17 @@ RSpec.shared_examples 'wiki controller actions' do describe 'PATCH #update' do let(:new_title) { 'New title' } let(:new_content) { 'New content' } + let(:id_param) { wiki_title } subject do patch(:update, params: routing_params.merge( - id: wiki_title, + id: id_param, wiki: { title: new_title, content: new_content } )) end - context 'when page content encoding is invalid' do - it 'redirects to show' do - allow(controller).to receive(:valid_encoding?).and_return(false) - - subject - expect(response).to redirect_to_wiki(wiki, wiki.list_pages.first) - end - end + it_behaves_like 'edit action' context 'when page content encoding is valid' do render_views diff --git a/spec/support/shared_examples/create_alert_issue_shared_examples.rb b/spec/support/shared_examples/create_alert_issue_shared_examples.rb new file mode 100644 index 00000000000..9f4e1c4335a --- /dev/null +++ b/spec/support/shared_examples/create_alert_issue_shared_examples.rb @@ -0,0 +1,27 @@ +# 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/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb index 6007798c290..9fc5d8933e5 100644 --- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb +++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb @@ -266,7 +266,7 @@ RSpec.shared_examples 'thread comments' do |resource_name| end end - it 'has "Comment" selected when opening the menu', quarantine: 'https://gitlab.com/gitlab-org/gitlab/issues/196825' do + it 'has "Comment" selected when opening the menu', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/196825' do find(toggle_selector).click find("#{menu_selector} li", match: :first) diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb index 964c80007b0..487c38da7da 100644 --- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb @@ -53,13 +53,17 @@ RSpec.shared_examples 'an editable merge request' do find('#merge_request_description').native.send_keys('') fill_in 'merge_request_description', with: user.to_reference[0..4] - wait_for_requests - page.within('.atwho-view') do expect(page).to have_content(user2.name) end end + it 'description has quick action autocomplete', :js do + find('#merge_request_description').native.send_keys('/') + + expect(page).to have_selector('.atwho-container') + end + it 'has class js-quick-submit in form' do expect(page).to have_selector('.js-quick-submit') end diff --git a/spec/support/shared_examples/features/error_tracking_shared_example.rb b/spec/support/shared_examples/features/error_tracking_shared_example.rb index 1cd05b22ae9..ae7d62f31a2 100644 --- a/spec/support/shared_examples/features/error_tracking_shared_example.rb +++ b/spec/support/shared_examples/features/error_tracking_shared_example.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'error tracking index page' do +RSpec.shared_examples 'error tracking index page' do it 'renders the error index page', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do within('div.js-title-container') do expect(page).to have_content(project.namespace.name) @@ -33,7 +33,7 @@ shared_examples 'error tracking index page' do end end -shared_examples 'expanded stack trace context' do |selected_line: nil, expected_line: 1| +RSpec.shared_examples 'expanded stack trace context' do |selected_line: nil, expected_line: 1| it 'expands the stack trace context', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do within('div.stacktrace') do find("div.file-holder:nth-child(#{selected_line}) svg.ic-chevron-right").click if selected_line @@ -48,7 +48,7 @@ shared_examples 'expanded stack trace context' do |selected_line: nil, expected_ end end -shared_examples 'error tracking show page' do +RSpec.shared_examples 'error tracking show page' do it 'renders the error details', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do content = page.find(".content") nav = page.find("nav.breadcrumbs") 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 98010150e65..00ce690d2e3 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 @@ -35,7 +35,12 @@ RSpec.shared_examples 'Maintainer manages access requests' do expect_visible_access_request(entity, user) - accept_confirm { click_on 'Deny access' } + # Open modal + click_on 'Deny access request' + + expect(page).not_to have_field "Also unassign this user from related issues and merge requests" + + click_on 'Deny access request' expect_no_visible_access_request(entity, user) expect(page).not_to have_content user.name diff --git a/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb index 2b96010477c..b2047f1d32c 100644 --- a/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb +++ b/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -shared_examples 'no Jira import data present' do +RSpec.shared_examples 'no Jira import data present' do it 'returns none' do expect(resolve_imports).to eq JiraImportState.none end end -shared_examples 'no Jira import access' do +RSpec.shared_examples 'no Jira import access' do it 'raises error' do expect do resolve_imports diff --git a/spec/support/shared_examples/graphql/mutation_shared_examples.rb b/spec/support/shared_examples/graphql/mutation_shared_examples.rb index 022d41c0bdd..86d2bb6c747 100644 --- a/spec/support/shared_examples/graphql/mutation_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutation_shared_examples.rb @@ -7,13 +7,23 @@ # # There must be a method or let called `mutation` defined that executes # the mutation. -RSpec.shared_examples 'a mutation that returns top-level errors' do |errors:| +RSpec.shared_examples 'a mutation that returns top-level errors' do |errors: []| + let(:match_errors) { eq(errors) } + it do post_graphql_mutation(mutation, current_user: current_user) error_messages = graphql_errors.map { |e| e['message'] } - expect(error_messages).to eq(errors) + expect(error_messages).to match_errors + end +end + +RSpec.shared_examples 'an invalid argument to the mutation' do |argument_name:| + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) do + contain_exactly(include("invalid value for #{GraphqlHelpers.fieldnamerize(argument_name)}")) + end end end diff --git a/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb index 4bed322564a..94b7ed1618d 100644 --- a/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb +++ b/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -shared_examples 'no project services' do +RSpec.shared_examples 'no project services' do it 'returns empty collection' do expect(resolve_services).to eq [] end end -shared_examples 'cannot access project services' do +RSpec.shared_examples 'cannot access project services' do it 'raises error' do expect do resolve_services diff --git a/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb b/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb index 58cd3d21f66..67d1c2a8254 100644 --- a/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb +++ b/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb @@ -1,7 +1,12 @@ # frozen_string_literal: true RSpec.shared_examples 'resolving an issuable in GraphQL' do |type| - subject { mutation.resolve_issuable(type: type, parent_path: parent.full_path, iid: issuable.iid) } + include GraphqlHelpers + + let(:parent_path) { parent.full_path } + let(:iid) { issuable.iid } + + subject(:result) { mutation.resolve_issuable(type: type, parent_path: parent_path, iid: iid) } context 'when user has access' do before do @@ -9,37 +14,23 @@ RSpec.shared_examples 'resolving an issuable in GraphQL' do |type| end it 'resolves issuable by iid' do - result = type == :merge_request ? subject.sync : subject expect(result).to eq(issuable) end - it 'uses the correct Resolver to resolve issuable' do - resolver_class = "Resolvers::#{type.to_s.classify.pluralize}Resolver".constantize - resolve_method = type == :epic ? :resolve_group : :resolve_project - resolved_parent = mutation.send(resolve_method, full_path: parent.full_path) - - allow(mutation).to receive(resolve_method) - .with(full_path: parent.full_path) - .and_return(resolved_parent) - - expect(resolver_class.single).to receive(:new) - .with(object: resolved_parent, context: context, field: nil) - .and_call_original - - subject - end - - it 'returns nil if issuable is not found' do - result = mutation.resolve_issuable(type: type, parent_path: parent.full_path, iid: "100") - result = result.respond_to?(:sync) ? result.sync : result + context 'the IID does not refer to a valid issuable' do + let(:iid) { '100' } - expect(result).to be_nil + it 'returns nil' do + expect(result).to be_nil + end end - it 'returns nil if parent path is not present' do - result = mutation.resolve_issuable(type: type, parent_path: "", iid: issuable.iid) + context 'the parent path is not present' do + let(:parent_path) { '' } - expect(result).to be_nil + it 'returns nil' do + expect(result).to be_nil + end end end end diff --git a/spec/support/shared_examples/helm_commands_shared_examples.rb b/spec/support/shared_examples/helm_commands_shared_examples.rb index f0624fbf29f..0a94c6648cc 100644 --- a/spec/support/shared_examples/helm_commands_shared_examples.rb +++ b/spec/support/shared_examples/helm_commands_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'helm command generator' do +RSpec.shared_examples 'helm command generator' do describe '#generate_script' do let(:helm_setup) do <<~EOS @@ -14,7 +14,7 @@ shared_examples 'helm command generator' do end end -shared_examples 'helm command' do +RSpec.shared_examples 'helm command' do describe '#rbac?' do subject { command.rbac? } 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 a40c38106e2..af65b61021c 100644 --- a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'resource mentions migration' do |migration_class, resource_class| +RSpec.shared_examples 'resource mentions migration' do |migration_class, resource_class| it 'migrates resource mentions' do join = migration_class::JOIN conditions = migration_class::QUERY_CONDITIONS @@ -21,7 +21,7 @@ shared_examples 'resource mentions migration' do |migration_class, resource_clas end end -shared_examples 'resource notes mentions migration' do |migration_class, resource_class| +RSpec.shared_examples 'resource notes mentions migration' do |migration_class, resource_class| it 'migrates mentions from note' do join = migration_class::JOIN conditions = migration_class::QUERY_CONDITIONS @@ -56,7 +56,7 @@ shared_examples 'resource notes mentions migration' do |migration_class, resourc end end -shared_examples 'schedules resource mentions migration' do |resource_class, is_for_notes| +RSpec.shared_examples 'schedules resource mentions migration' do |resource_class, is_for_notes| before do stub_const("#{described_class.name}::BATCH_SIZE", 1) end diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb index 14292f70228..d76089d56dd 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb @@ -4,7 +4,7 @@ require 'spec_helper' ISSUES_MEDIAN = 30.minutes.to_i -shared_examples 'base stage' do +RSpec.shared_examples 'base stage' do let(:stage) { described_class.new(options: { project: double }) } before do @@ -35,7 +35,7 @@ shared_examples 'base stage' do end end -shared_examples 'calculate #median with date range' do +RSpec.shared_examples 'calculate #median with date range' do context 'when valid date range is given' do before do stage_options[:from] = 5.days.ago @@ -55,7 +55,7 @@ shared_examples 'calculate #median with date range' do end end -shared_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do +RSpec.shared_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do let(:stage_params) { Gitlab::Analytics::CycleAnalytics::DefaultStages.send("params_for_#{stage_name}_stage").merge(project: project) } let(:stage) { Analytics::CycleAnalytics::ProjectStage.new(stage_params) } let(:data_collector) { Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: stage, params: { from: stage_options[:from], current_user: project.creator }) } diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/default_query_config_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/default_query_config_shared_examples.rb index c053af010b3..4f648b27ea2 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/default_query_config_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/default_query_config_shared_examples.rb @@ -2,7 +2,7 @@ require 'spec_helper' -shared_examples 'default query config' do +RSpec.shared_examples 'default query config' do let(:project) { create(:project) } let(:event) { described_class.new(stage: stage_name, options: { from: 1.day.ago, project: project }) } diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb index a00359ce979..d0e41605e00 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb @@ -8,6 +8,7 @@ RSpec.shared_examples_for 'cycle analytics event' do it { expect(described_class.identifier).to be_a_kind_of(Symbol) } it { expect(instance.object_type.ancestors).to include(ApplicationRecord) } it { expect(instance).to respond_to(:timestamp_projection) } + it { expect(instance.column_list).to be_a_kind_of(Array) } describe '#apply_query_customization' do it 'expects an ActiveRecord::Relation object as argument and returns a modified version of it' do diff --git a/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb index a1cdd054f32..e43ce936b90 100644 --- a/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb @@ -10,7 +10,7 @@ RSpec.shared_examples 'diff statistics' do |test_include_stats_flag: true| end end - context 'when should request diff stats' do + context 'when include_stats is true' do it 'Repository#diff_stats is called' do expect(diffable.project.repository) .to receive(:diff_stats) @@ -59,43 +59,87 @@ RSpec.shared_examples 'unfoldable diff' do end RSpec.shared_examples 'cacheable diff collection' do - let(:cache) { instance_double(Gitlab::Diff::HighlightCache) } + let(:highlight_cache) { instance_double(Gitlab::Diff::HighlightCache, write_if_empty: true, clear: nil, decorate: nil) } + let(:stats_cache) { instance_double(Gitlab::Diff::StatsCache, read: nil, write_if_empty: true, clear: nil) } before do - expect(Gitlab::Diff::HighlightCache).to receive(:new).with(subject) { cache } + expect(Gitlab::Diff::HighlightCache).to receive(:new).with(subject) { highlight_cache } end describe '#write_cache' do + before do + expect(Gitlab::Diff::StatsCache).to receive(:new).with(cachable_key: diffable.cache_key) { stats_cache } + end + it 'calls Gitlab::Diff::HighlightCache#write_if_empty' do - expect(cache).to receive(:write_if_empty).once + expect(highlight_cache).to receive(:write_if_empty).once + + subject.write_cache + end + + it 'calls Gitlab::Diff::StatsCache#write_if_empty with diff stats' do + diff_stats = Gitlab::Git::DiffStatsCollection.new([]) + + expect(diffable.project.repository) + .to receive(:diff_stats).and_return(diff_stats) + + expect(stats_cache).to receive(:write_if_empty).once.with(diff_stats) subject.write_cache end end describe '#clear_cache' do + before do + expect(Gitlab::Diff::StatsCache).to receive(:new).with(cachable_key: diffable.cache_key) { stats_cache } + end + it 'calls Gitlab::Diff::HighlightCache#clear' do - expect(cache).to receive(:clear).once + expect(highlight_cache).to receive(:clear).once subject.clear_cache end - end - describe '#cache_key' do - it 'calls Gitlab::Diff::HighlightCache#key' do - expect(cache).to receive(:key).once + it 'calls Gitlab::Diff::StatsCache#clear' do + expect(stats_cache).to receive(:clear).once - subject.cache_key + subject.clear_cache end end describe '#diff_files' do + before do + expect(Gitlab::Diff::StatsCache).to receive(:new).with(cachable_key: diffable.cache_key) { stats_cache } + end + it 'calls Gitlab::Diff::HighlightCache#decorate' do - expect(cache).to receive(:decorate) + expect(highlight_cache).to receive(:decorate) .with(instance_of(Gitlab::Diff::File)) .exactly(cacheable_files_count).times subject.diff_files end + + context 'when there are stats cached' do + before do + allow(stats_cache).to receive(:read).and_return(Gitlab::Git::DiffStatsCollection.new([])) + end + + it 'does not make a diff stats rpc call' do + expect(diffable.project.repository).not_to receive(:diff_stats) + + subject.diff_files + end + end + + context 'when there are no stats cached' do + it 'makes a diff stats rpc call' do + expect(diffable.project.repository) + .to receive(:diff_stats) + .with(diffable.diff_refs.base_sha, diffable.diff_refs.head_sha) + + subject.diff_files + end + end end end diff --git a/spec/support/shared_examples/lib/gitlab/gl_repository_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/gl_repository_shared_examples.rb index 97f4341340d..28137530038 100644 --- a/spec/support/shared_examples/lib/gitlab/gl_repository_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/gl_repository_shared_examples.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true RSpec.shared_examples 'parsing gl_repository identifier' do - subject { described_class.new(identifier) } + subject { described_class.parse(identifier) } it 'returns correct information' do - aggregate_failures do - expect(subject.repo_type).to eq(expected_type) - expect(subject.fetch_container!).to eq(expected_container) - end + expect(subject).to have_attributes( + repo_type: expected_type, + container: expected_container + ) end end diff --git a/spec/support/shared_examples/lib/gitlab/import/stuck_import_job_workers_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import/stuck_import_job_workers_shared_examples.rb index 06ea540706a..222390cf9cd 100644 --- a/spec/support/shared_examples/lib/gitlab/import/stuck_import_job_workers_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/import/stuck_import_job_workers_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'stuck import job detection' do +RSpec.shared_examples 'stuck import job detection' do context 'when the job has completed' do context 'when the import status was already updated' do before do diff --git a/spec/support/shared_examples/lib/gitlab/jira_import/base_importer_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/jira_import/base_importer_shared_examples.rb index 85dcc053447..b1788bb5912 100644 --- a/spec/support/shared_examples/lib/gitlab/jira_import/base_importer_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/jira_import/base_importer_shared_examples.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -shared_examples 'raise exception if not implemented' do +RSpec.shared_examples 'raise exception if not implemented' do it { expect { described_class.new(project).imported_items_cache_key }.not_to raise_error } end diff --git a/spec/support/shared_examples/lib/wikis_api_examples.rb b/spec/support/shared_examples/lib/wikis_api_examples.rb new file mode 100644 index 00000000000..2e4c667d37e --- /dev/null +++ b/spec/support/shared_examples/lib/wikis_api_examples.rb @@ -0,0 +1,174 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'wikis API returns list of wiki pages' do + context 'when wiki has pages' do + let!(:pages) do + [create(:wiki_page, wiki: wiki, title: 'page1', content: 'content of page1'), + create(:wiki_page, wiki: wiki, title: 'page2.with.dot', content: 'content of page2')] + end + + it 'returns the list of wiki pages without content' do + get api(url, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(2) + + json_response.each_with_index do |page, index| + expect(page.keys).to match_array(expected_keys_without_content) + expect(page['slug']).to eq(pages[index].slug) + expect(page['title']).to eq(pages[index].title) + end + end + + it 'returns the list of wiki pages with content' do + get api(url, user), params: { with_content: 1 } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(2) + + json_response.each_with_index do |page, index| + expect(page.keys).to match_array(expected_keys_with_content) + expect(page['content']).to eq(pages[index].content) + expect(page['slug']).to eq(pages[index].slug) + expect(page['title']).to eq(pages[index].title) + end + end + end + + it 'return the empty list of wiki pages' do + get api(url, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(0) + end +end + +RSpec.shared_examples_for 'wikis API returns wiki page' do + it 'returns the wiki page' do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(4) + expect(json_response.keys).to match_array(expected_keys_with_content) + expect(json_response['content']).to eq(page.content) + expect(json_response['slug']).to eq(page.slug) + expect(json_response['title']).to eq(page.title) + end +end + +RSpec.shared_examples_for 'wikis API creates wiki page' do + it 'creates the wiki page' do + post(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response.size).to eq(4) + expect(json_response.keys).to match_array(expected_keys_with_content) + expect(json_response['content']).to eq(payload[:content]) + expect(json_response['slug']).to eq(payload[:title].tr(' ', '-')) + expect(json_response['title']).to eq(payload[:title]) + expect(json_response['rdoc']).to eq(payload[:rdoc]) + end + + [:title, :content].each do |part| + it "responds with validation error on empty #{part}" do + payload.delete(part) + + post(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response.size).to eq(1) + expect(json_response['error']).to eq("#{part} is missing") + end + end +end + +RSpec.shared_examples_for 'wikis API updates wiki page' do + it 'updates the wiki page' do + put(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(4) + expect(json_response.keys).to match_array(expected_keys_with_content) + expect(json_response['content']).to eq(payload[:content]) + expect(json_response['slug']).to eq(payload[:title].tr(' ', '-')) + expect(json_response['title']).to eq(payload[:title]) + end + + [:title, :content, :format].each do |part| + it "updates with wiki with missing #{part}" do + payload.delete(part) + + put(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(:ok) + end + end +end + +RSpec.shared_examples_for 'wiki API 403 Forbidden' do + it 'returns 403 Forbidden' do + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response.size).to eq(1) + expect(json_response['message']).to eq('403 Forbidden') + end +end + +RSpec.shared_examples_for 'wiki API 404 Wiki Page Not Found' do + it 'returns 404 Wiki Page Not Found' do + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response.size).to eq(1) + expect(json_response['message']).to eq('404 Wiki Page Not Found') + end +end + +RSpec.shared_examples_for 'wiki API 404 Not Found' do |what| + it "returns 404 #{what} Not Found" do + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response.size).to eq(1) + expect(json_response['message']).to eq("404 #{what} Not Found") + end +end + +RSpec.shared_examples_for 'wiki API 204 No Content' do + it 'returns 204 No Content' do + expect(response).to have_gitlab_http_status(:no_content) + end +end + +RSpec.shared_examples_for 'wiki API uploads wiki attachment' do + it 'pushes attachment to the wiki repository' do + allow(SecureRandom).to receive(:hex).and_return('fixed_hex') + + workhorse_post_with_file(api(url, user), file_key: :file, params: payload) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to eq result_hash.deep_stringify_keys + end + + it 'responds with validation error on empty file' do + payload.delete(:file) + + post(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response.size).to eq(1) + expect(json_response['error']).to eq('file is missing') + end + + it 'responds with validation error on invalid temp file' do + payload[:file] = { tempfile: '/etc/hosts' } + + post(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response.size).to eq(1) + expect(json_response['error']).to eq('file is invalid') + end + + it 'is backward compatible with regular multipart uploads' do + allow(SecureRandom).to receive(:hex).and_return('fixed_hex') + + post(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to eq result_hash.deep_stringify_keys + 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 0efa5e56199..f80ca235220 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 @@ -88,16 +88,6 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| end end - it 'sets the correct version of the application' do - subject.update!(version: '0.0.0') - - subject.make_installed! - - subject.reload - - expect(subject.version).to eq(subject.class.const_get(:VERSION, false)) - end - context 'application is updating' do subject { create(application_name, :updating) } @@ -146,16 +136,6 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| end end end - - it 'updates the version of the application' do - subject.update!(version: '0.0.0') - - subject.make_installed! - - subject.reload - - expect(subject.version).to eq(subject.class.const_get(:VERSION, false)) - end end end diff --git a/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb index cf7010c48c2..ed2e4fee2de 100644 --- a/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb @@ -19,4 +19,32 @@ RSpec.shared_examples 'cluster application version specs' do |application_name| it { is_expected.to be_falsey } end end + + describe '#make_installed' do + subject { create(application_name, :installing) } + + it 'sets the correct version of the application' do + subject.update!(version: '0.0.0') + + subject.make_installed! + + subject.reload + + expect(subject.version).to eq(subject.class.const_get(:VERSION, false)) + end + + context 'application is updating' do + subject { create(application_name, :updating) } + + it 'updates the version of the application' do + subject.update!(version: '0.0.0') + + subject.make_installed! + + subject.reload + + expect(subject.version).to eq(subject.class.const_get(:VERSION, false)) + end + end + end end diff --git a/spec/support/shared_examples/models/concerns/bulk_insert_safe_shared_examples.rb b/spec/support/shared_examples/models/concerns/bulk_insert_safe_shared_examples.rb index 7bcd6191f1d..3db5d7a8d7d 100644 --- a/spec/support/shared_examples/models/concerns/bulk_insert_safe_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/bulk_insert_safe_shared_examples.rb @@ -7,17 +7,17 @@ RSpec.shared_examples 'a BulkInsertSafe model' do |klass| let(:target_class) { klass.dup } # We consider all callbacks unsafe for bulk insertions unless we have explicitly - # whitelisted them (esp. anything related to :save, :create, :commit etc.) - let(:callback_method_blacklist) do + # allowed them (especially anything related to :save, :create, :commit, etc.) + let(:unsafe_callbacks) do ActiveRecord::Callbacks::CALLBACKS.reject do |callback| cb_name = callback.to_s.gsub(/(before_|after_|around_)/, '').to_sym - BulkInsertSafe::CALLBACK_NAME_WHITELIST.include?(cb_name) + BulkInsertSafe::ALLOWED_CALLBACKS.include?(cb_name) end.to_set end context 'when calling class methods directly' do it 'raises an error when method is not bulk-insert safe' do - callback_method_blacklist.each do |m| + unsafe_callbacks.each do |m| expect { target_class.send(m, nil) }.to( raise_error(BulkInsertSafe::MethodNotAllowedError), "Expected call to #{m} to raise an error, but it didn't" @@ -26,7 +26,7 @@ RSpec.shared_examples 'a BulkInsertSafe model' do |klass| end it 'does not raise an error when method is bulk-insert safe' do - BulkInsertSafe::CALLBACK_NAME_WHITELIST.each do |name| + BulkInsertSafe::ALLOWED_CALLBACKS.each do |name| expect { target_class.set_callback(name) {} }.not_to raise_error end end diff --git a/spec/support/shared_examples/models/jira_import_state_shared_examples.rb b/spec/support/shared_examples/models/jira_import_state_shared_examples.rb index f4643375c8e..1999f0f3c49 100644 --- a/spec/support/shared_examples/models/jira_import_state_shared_examples.rb +++ b/spec/support/shared_examples/models/jira_import_state_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'multiple running imports not allowed' do +RSpec.shared_examples 'multiple running imports not allowed' do it 'returns not valid' do new_import = build(:jira_import_state, project: project) @@ -9,21 +9,21 @@ shared_examples 'multiple running imports not allowed' do end end -shared_examples 'in progress' do |status| +RSpec.shared_examples 'in progress' do |status| it 'returns true' do jira_import_state = build(:jira_import_state, status: status) expect(jira_import_state).to be_in_progress end end -shared_examples 'not in progress' do |status| +RSpec.shared_examples 'not in progress' do |status| it 'returns false' do jira_import_state = build(:jira_import_state, status: status) expect(jira_import_state).not_to be_in_progress end end -shared_examples 'can transition' do |states| +RSpec.shared_examples 'can transition' do |states| states.each do |state| it 'returns true' do expect(jira_import.send(state)).to be true @@ -31,7 +31,7 @@ shared_examples 'can transition' do |states| end end -shared_examples 'cannot transition' do |states| +RSpec.shared_examples 'cannot transition' do |states| states.each do |state| it 'returns false' do expect(jira_import.send(state)).to be false diff --git a/spec/support/shared_examples/models/note_access_check_shared_examples.rb b/spec/support/shared_examples/models/note_access_check_shared_examples.rb index 3bafad202f6..44edafe9091 100644 --- a/spec/support/shared_examples/models/note_access_check_shared_examples.rb +++ b/spec/support/shared_examples/models/note_access_check_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'users with note access' do +RSpec.shared_examples 'users with note access' do it 'returns true' do users.each do |user| expect(note.system_note_with_references_visible_for?(user)).to be_truthy @@ -9,7 +9,7 @@ shared_examples 'users with note access' do end end -shared_examples 'users without note access' do +RSpec.shared_examples 'users without note access' do it 'returns false' do users.each do |user| expect(note.system_note_with_references_visible_for?(user)).to be_falsy diff --git a/spec/support/shared_examples/models/services_fields_shared_examples.rb b/spec/support/shared_examples/models/services_fields_shared_examples.rb deleted file mode 100644 index cb36f74460d..00000000000 --- a/spec/support/shared_examples/models/services_fields_shared_examples.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'issue tracker fields' do - let(:title) { 'custom title' } - let(:description) { 'custom description' } - let(:url) { 'http://issue_tracker.example.com' } - - context 'when data are stored in the properties' do - describe '#update' do - before do - service.update(title: 'new_title', description: 'new description') - end - - it 'removes title and description from properties' do - expect(service.reload.properties).not_to include('title', 'description') - end - - it 'stores title & description in services table' do - expect(service.read_attribute(:title)).to eq('new_title') - expect(service.read_attribute(:description)).to eq('new description') - end - end - - describe 'reading fields' do - it 'returns correct values' do - expect(service.title).to eq(title) - expect(service.description).to eq(description) - end - end - end -end diff --git a/spec/support/shared_examples/models/synthetic_note_shared_examples.rb b/spec/support/shared_examples/models/synthetic_note_shared_examples.rb new file mode 100644 index 00000000000..a41ade2950a --- /dev/null +++ b/spec/support/shared_examples/models/synthetic_note_shared_examples.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a synthetic note' do |action| + it_behaves_like 'a system note', exclude_project: true do + let(:action) { action } + end + + describe '#discussion_id' do + before do + allow(event).to receive(:discussion_id).and_return('foobar42') + end + + it 'returns the expected discussion id' do + expect(subject.discussion_id(nil)).to eq('foobar42') + end + end +end diff --git a/spec/support/shared_examples/namespaces/hierarchy_examples.rb b/spec/support/shared_examples/namespaces/hierarchy_examples.rb new file mode 100644 index 00000000000..d5754f47be2 --- /dev/null +++ b/spec/support/shared_examples/namespaces/hierarchy_examples.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'hierarchy with traversal_ids' do + # A convenient null node to represent the parent of root. + let(:null_node) { double(traversal_ids: []) } + + # Walk the tree to assert that the current_node's traversal_id is always + # present and equal to it's parent's traversal_ids plus it's own ID. + def validate_traversal_ids(current_node, parent = null_node) + expect(current_node.traversal_ids).to be_present + expect(current_node.traversal_ids).to eq parent.traversal_ids + [current_node.id] + + current_node.children.each do |child| + validate_traversal_ids(child, current_node) + end + end + + it 'will be valid' do + validate_traversal_ids(root) + end +end diff --git a/spec/support/shared_examples/policies/namespace_policy_shared_examples.rb b/spec/support/shared_examples/policies/namespace_policy_shared_examples.rb new file mode 100644 index 00000000000..ddec1ba5e3d --- /dev/null +++ b/spec/support/shared_examples/policies/namespace_policy_shared_examples.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'update namespace limit policy' do + describe 'update_subscription_limit' do + using RSpec::Parameterized::TableSyntax + + let(:policy) { :update_subscription_limit } + + where(:role, :is_com, :allowed) do + :user | true | false + :owner | true | false + :admin | true | true + :user | false | false + :owner | false | false + :admin | false | false + end + + with_them do + let(:current_user) { build_stubbed(role) } + + before do + allow(Gitlab).to receive(:com?).and_return(is_com) + end + + context 'when admin mode enabled', :enable_admin_mode do + it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) } + end + + context 'when admin mode disabled' do + it { is_expected.to be_disallowed(policy) } + 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 4dd0152e3d1..f8526ec68dc 100644 --- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb +++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb @@ -41,6 +41,28 @@ RSpec.shared_examples 'archived project policies' do end end +RSpec.shared_examples 'project private features with read_all_resources ability' do + subject { described_class.new(user, project) } + + before do + project.project_feature.update!( + repository_access_level: ProjectFeature::PRIVATE, + merge_requests_access_level: ProjectFeature::PRIVATE, + builds_access_level: ProjectFeature::PRIVATE + ) + end + + [:public, :internal, :private].each do |visibility| + context "for #{visibility} projects" do + let(:project) { create(:project, visibility, namespace: owner.namespace) } + + it 'allows the download_code ability' do + expect_allowed(:download_code) + end + end + end +end + RSpec.shared_examples 'project policies as anonymous' do context 'abilities for public projects' do context 'when a project has pending invites' do @@ -231,6 +253,12 @@ RSpec.shared_examples 'project policies as admin with admin mode' do let(:regular_abilities) { owner_permissions } end end + + context 'abilities for all project visibility', :enable_admin_mode do + it_behaves_like 'project private features with read_all_resources ability' do + let(:user) { admin } + end + end end RSpec.shared_examples 'project policies as admin without admin mode' do diff --git a/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb index 159660e7d1d..910805dbdea 100644 --- a/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb @@ -47,7 +47,7 @@ RSpec.shared_examples 'create_merge_request quick action' do expect(created_mr.source_branch).to eq(issue.to_branch_name) visit project_merge_request_path(project, created_mr) - expect(page).to have_content %{WIP: Resolve "#{issue.title}"} + expect(page).to have_content %{Draft: Resolve "#{issue.title}"} end it 'creates a merge request using the given branch name' do @@ -60,7 +60,7 @@ RSpec.shared_examples 'create_merge_request quick action' do expect(created_mr.source_branch).to eq(branch_name) visit project_merge_request_path(project, created_mr) - expect(page).to have_content %{WIP: Resolve "#{issue.title}"} + expect(page).to have_content %{Draft: Resolve "#{issue.title}"} end end end diff --git a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb index e0edbc5637a..258d9ab85e4 100644 --- a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb @@ -1,68 +1,85 @@ # frozen_string_literal: true RSpec.shared_examples 'merge quick action' do - context 'when the current user can merge the MR' do + context 'when updating the description' do before do sign_in(user) - visit project_merge_request_path(project, merge_request) + visit edit_project_merge_request_path(project, merge_request) end - it 'merges the MR', :sidekiq_might_not_need_inline do - add_note("/merge") - - expect(page).to have_content 'Merged this merge request.' + it 'merges the MR', :sidekiq_inline do + fill_in('Description', with: '/merge') + click_button('Save changes') + expect(page).to have_content('Merged') expect(merge_request.reload).to be_merged end + end - context 'when auto merge is avialable' do + context 'when creating a new note' do + context 'when the current user can merge the MR' do before do - create(:ci_pipeline, :detached_merge_request_pipeline, - project: project, merge_request: merge_request) - merge_request.update_head_pipeline + sign_in(user) + visit project_merge_request_path(project, merge_request) end - it 'schedules to merge the MR' do + it 'merges the MR', :sidekiq_inline do add_note("/merge") - expect(page).to have_content "Scheduled to merge this merge request (Merge when pipeline succeeds)." + expect(page).to have_content 'Merged this merge request.' - expect(merge_request.reload).to be_auto_merge_enabled - expect(merge_request.reload).not_to be_merged + expect(merge_request.reload).to be_merged end - end - end - context 'when the head diff changes in the meanwhile' do - before do - merge_request.source_branch = 'another_branch' - merge_request.save - sign_in(user) - visit project_merge_request_path(project, merge_request) - end + context 'when auto merge is available' do + before do + create(:ci_pipeline, :detached_merge_request_pipeline, + project: project, merge_request: merge_request) + merge_request.update_head_pipeline + end - it 'does not merge the MR' do - add_note("/merge") + it 'schedules to merge the MR' do + add_note("/merge") - expect(page).not_to have_content 'Your commands have been executed!' + expect(page).to have_content "Scheduled to merge this merge request (Merge when pipeline succeeds)." - expect(merge_request.reload).not_to be_merged + expect(merge_request.reload).to be_auto_merge_enabled + expect(merge_request.reload).not_to be_merged + end + end end - end - context 'when the current user cannot merge the MR' do - before do - project.add_guest(guest) - sign_in(guest) - visit project_merge_request_path(project, merge_request) + context 'when the head diff changes in the meanwhile' do + before do + merge_request.source_branch = 'another_branch' + merge_request.save + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + + it 'does not merge the MR' do + add_note("/merge") + + expect(page).not_to have_content 'Your commands have been executed!' + + expect(merge_request.reload).not_to be_merged + end end - it 'does not merge the MR' do - add_note("/merge") + context 'when the current user cannot merge the MR' do + before do + project.add_guest(guest) + sign_in(guest) + visit project_merge_request_path(project, merge_request) + end + + it 'does not merge the MR' do + add_note("/merge") - expect(page).not_to have_content 'Your commands have been executed!' + expect(page).not_to have_content 'Your commands have been executed!' - expect(merge_request.reload).not_to be_merged + expect(merge_request.reload).not_to be_merged + end end end end diff --git a/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb new file mode 100644 index 00000000000..5257980d7df --- /dev/null +++ b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +RSpec.shared_context 'Composer user type' do |user_type, add_member| + before do + group.send("add_#{user_type}", user) if add_member && user_type != :anonymous + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end +end + +RSpec.shared_examples 'Composer package index' do |user_type, status, add_member = true| + include_context 'Composer user type', user_type, add_member do + 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') + end + end +end + +RSpec.shared_examples 'Composer empty provider index' do |user_type, status, add_member = true| + include_context 'Composer user type', user_type, add_member do + 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/provider') + expect(json_response['providers']).to eq({}) + end + end +end + +RSpec.shared_examples 'Composer provider index' do |user_type, status, add_member = true| + include_context 'Composer user type', user_type, add_member do + 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/provider') + expect(json_response['providers']).to include(package.name) + end + end +end + +RSpec.shared_examples 'Composer package api request' do |user_type, status, add_member = true| + include_context 'Composer user type', user_type, add_member do + 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/package') + expect(json_response['packages']).to include(package.name) + expect(json_response['packages'][package.name]).to include(package.version) + end + end +end + +RSpec.shared_examples 'Composer package creation' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + group.send("add_#{user_type}", user) if add_member && user_type != :anonymous + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it 'creates package files' do + expect { subject } + .to change { project.packages.composer.count }.by(1) + + expect(response).to have_gitlab_http_status(status) + end + it_behaves_like 'a gitlab tracking event', described_class.name, 'register_package' + end +end + +RSpec.shared_examples 'process Composer api request' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + group.send("add_#{user_type}", user) if add_member && user_type != :anonymous + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + end +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) } +end + +RSpec.shared_context 'Composer api project access' do |project_visibility_level, user_role, user_token| + include_context 'Composer auth headers', user_role, user_token do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + end +end + +RSpec.shared_context 'Composer api group access' do |project_visibility_level, user_role, user_token| + include_context 'Composer auth headers', user_role, user_token do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + end +end + +RSpec.shared_examples 'rejects Composer access with unknown group id' do + context 'with an unknown group' do + let(:group) { double(id: non_existing_record_id) } + + context 'as anonymous' do + it_behaves_like 'process Composer api request', :anonymous, :not_found + end + + context 'as authenticated user' do + subject { get api(url), headers: build_basic_auth_header(user.username, personal_access_token.token) } + + it_behaves_like 'process Composer api request', :anonymous, :not_found + end + end +end + +RSpec.shared_examples 'rejects Composer access with unknown project id' do + context 'with an unknown project' do + let(:project) { double(id: non_existing_record_id) } + + context 'as anonymous' do + it_behaves_like 'process Composer api request', :anonymous, :not_found + end + + context 'as authenticated user' do + subject { get api(url), headers: build_basic_auth_header(user.username, personal_access_token.token) } + + it_behaves_like 'process Composer api request', :anonymous, :not_found + end + end +end diff --git a/spec/support/shared_examples/requests/api/graphql/projects/services_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/projects/services_shared_examples.rb index 246f1850c3c..da1caef63ba 100644 --- a/spec/support/shared_examples/requests/api/graphql/projects/services_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/projects/services_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'unauthorized users cannot read services' do +RSpec.shared_examples 'unauthorized users cannot read services' do before do post_graphql(query, current_user: current_user) end 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 60ed61269df..a34c48a5ba4 100644 --- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb @@ -132,6 +132,16 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| expect(response).to have_gitlab_http_status(:created) expect(json_response['body']).to eq('hi!') + expect(json_response['confidential']).to be_falsey + expect(json_response['author']['username']).to eq(user.username) + end + + it "creates a confidential note if confidential is set to true" do + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: { body: 'hi!', confidential: true } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['body']).to eq('hi!') + expect(json_response['confidential']).to be_truthy expect(json_response['author']['username']).to eq(user.username) 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 new file mode 100644 index 00000000000..8d8483cae72 --- /dev/null +++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb @@ -0,0 +1,408 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'rejects nuget packages access' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + if status == :unauthorized + it 'has the correct response header' do + subject + + expect(response.headers['Www-Authenticate: Basic realm']).to eq 'GitLab Packages Registry' + end + end + end +end + +RSpec.shared_examples 'process nuget service index request' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + it_behaves_like 'a gitlab tracking event', described_class.name, 'nuget_service_index' + + it 'returns a valid json response' do + subject + + expect(response.media_type).to eq('application/json') + expect(json_response).to match_schema('public_api/v4/packages/nuget/service_index') + expect(json_response).to be_a(Hash) + end + + context 'with invalid format' do + let(:url) { "/projects/#{project.id}/packages/nuget/index.xls" } + + it_behaves_like 'rejects nuget packages access', :anonymous, :not_found + end + end +end + +RSpec.shared_examples 'returning nuget metadata json response with json schema' do |json_schema| + it 'returns a valid json response' do + subject + + expect(response.media_type).to eq('application/json') + expect(json_response).to match_schema(json_schema) + expect(json_response).to be_a(Hash) + end +end + +RSpec.shared_examples 'process nuget metadata request at package name level' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + it_behaves_like 'returning nuget metadata json response with json schema', 'public_api/v4/packages/nuget/packages_metadata' + + context 'with invalid format' do + let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/index.xls" } + + it_behaves_like 'rejects nuget packages access', :anonymous, :not_found + end + + context 'with lower case package name' do + let_it_be(:package_name) { 'dummy.package' } + + it_behaves_like 'returning response status', status + + it_behaves_like 'returning nuget metadata json response with json schema', 'public_api/v4/packages/nuget/packages_metadata' + end + end +end + +RSpec.shared_examples 'process nuget metadata request at package name and package version level' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + it_behaves_like 'returning nuget metadata json response with json schema', 'public_api/v4/packages/nuget/package_metadata' + + context 'with invalid format' do + let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/#{package.version}.xls" } + + it_behaves_like 'rejects nuget packages access', :anonymous, :not_found + end + + context 'with lower case package name' do + let_it_be(:package_name) { 'dummy.package' } + + it_behaves_like 'returning response status', status + + it_behaves_like 'returning nuget metadata json response with json schema', 'public_api/v4/packages/nuget/package_metadata' + end + end +end + +RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + it 'has the proper content type' do + subject + + expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + end + + context 'with a request that bypassed gitlab-workhorse' do + let(:headers) do + build_basic_auth_header(user.username, personal_access_token.token) + .merge(workhorse_header) + .tap { |h| h.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) } + end + + before do + project.add_maintainer(user) + end + + it_behaves_like 'returning response status', :forbidden + end + end +end + +RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = true| + RSpec.shared_examples 'creates nuget package files' do + it 'creates package files' do + expect(::Packages::Nuget::ExtractionWorker).to receive(:perform_async).once + expect { subject } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + expect(response).to have_gitlab_http_status(status) + + package_file = project.packages.last.package_files.reload.last + expect(package_file.file_name).to eq('package.nupkg') + end + end + + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + context 'with object storage disabled' do + before do + stub_package_file_object_storage(enabled: false) + end + + context 'without a file from workhorse' do + let(:send_rewritten_field) { false } + + it_behaves_like 'returning response status', :bad_request + end + + context 'with correct params' do + it_behaves_like 'package workhorse uploads' + it_behaves_like 'creates nuget package files' + it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package' + end + end + + context 'with object storage enabled' do + let(:tmp_object) do + fog_connection.directories.new(key: 'packages').files.create( + key: "tmp/uploads/#{file_name}", + body: 'content' + ) + end + let(:fog_file) { fog_to_uploaded_file(tmp_object) } + let(:params) { { package: fog_file, 'package.remote_id' => file_name } } + + context 'and direct upload enabled' do + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: true) + end + + it_behaves_like 'creates nuget package files' + + ['123123', '../../123123'].each do |remote_id| + context "with invalid remote_id: #{remote_id}" do + let(:params) do + { + package: fog_file, + 'package.remote_id' => remote_id + } + end + + it_behaves_like 'returning response status', :forbidden + end + end + + context 'with crafted package.path param' do + let(:crafted_file) { Tempfile.new('nuget.crafted.package.path') } + let(:url) { "/projects/#{project.id}/packages/nuget?package.path=#{crafted_file.path}" } + let(:params) { { file: temp_file(file_name) } } + let(:file_key) { :file } + + it 'does not create a package file' do + expect { subject }.to change { ::Packages::PackageFile.count }.by(0) + end + + it_behaves_like 'returning response status', :bad_request + end + end + + context 'and direct upload disabled' do + context 'and background upload disabled' do + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: false, background_upload: false) + end + + it_behaves_like 'creates nuget package files' + end + + context 'and background upload enabled' do + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: false, background_upload: true) + end + + it_behaves_like 'creates nuget package files' + end + end + end + + it_behaves_like 'background upload schedules a file migration' + end +end + +RSpec.shared_examples 'process nuget download versions request' do |user_type, status, add_member = true| + RSpec.shared_examples 'returns a valid nuget download versions json response' do + it 'returns a valid json response' do + subject + + expect(response.media_type).to eq('application/json') + expect(json_response).to match_schema('public_api/v4/packages/nuget/download_versions') + expect(json_response).to be_a(Hash) + expect(json_response['versions']).to match_array(packages.map(&:version).sort) + end + end + + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + it_behaves_like 'returns a valid nuget download versions json response' + + context 'with invalid format' do + let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package_name}/index.xls" } + + it_behaves_like 'rejects nuget packages access', :anonymous, :not_found + end + + context 'with lower case package name' do + let_it_be(:package_name) { 'dummy.package' } + + it_behaves_like 'returning response status', status + + it_behaves_like 'returns a valid nuget download versions json response' + end + end +end + +RSpec.shared_examples 'process nuget download content request' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package' + + it 'returns a valid package archive' do + subject + + expect(response.media_type).to eq('application/octet-stream') + end + + context 'with invalid format' do + let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.name}.#{package.version}.xls" } + + it_behaves_like 'rejects nuget packages access', :anonymous, :not_found + end + + context 'with lower case package name' do + let_it_be(:package_name) { 'dummy.package' } + + it_behaves_like 'returning response status', status + + it 'returns a valid package archive' do + subject + + expect(response.media_type).to eq('application/octet-stream') + end + end + end +end + +RSpec.shared_examples 'process nuget search request' do |user_type, status, add_member = true| + RSpec.shared_examples 'returns a valid json search response' do |status, total_hits, versions| + it_behaves_like 'returning response status', status + + it 'returns a valid json response' do + subject + + expect(response.media_type).to eq('application/json') + expect(json_response).to be_a(Hash) + expect(json_response).to match_schema('public_api/v4/packages/nuget/search') + expect(json_response['totalHits']).to eq total_hits + expect(json_response['data'].map { |e| e['versions'].size }).to match_array(versions) + end + end + + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returns a valid json search response', status, 4, [1, 5, 5, 1] + + it_behaves_like 'a gitlab tracking event', described_class.name, 'search_package' + + context 'with skip set to 2' do + let(:skip) { 2 } + + it_behaves_like 'returns a valid json search response', status, 4, [5, 1] + end + + context 'with take set to 2' do + let(:take) { 2 } + + it_behaves_like 'returns a valid json search response', status, 4, [1, 5] + end + + context 'without prereleases' do + let(:include_prereleases) { false } + + it_behaves_like 'returns a valid json search response', status, 3, [1, 5, 5] + end + + context 'with empty search term' do + let(:search_term) { '' } + + it_behaves_like 'returns a valid json search response', status, 5, [1, 5, 5, 1, 1] + end + + context 'with nil search term' do + let(:search_term) { nil } + + it_behaves_like 'returns a valid json search response', status, 5, [1, 5, 5, 1, 1] + end + end +end + +RSpec.shared_examples 'rejects nuget access with invalid project id' do + context 'with a project id with invalid integers' do + using RSpec::Parameterized::TableSyntax + + let(:project) { OpenStruct.new(id: id) } + + where(:id, :status) do + '/../' | :unauthorized + '' | :not_found + '%20' | :unauthorized + '%2e%2e%2f' | :unauthorized + 'NaN' | :unauthorized + 00002345 | :unauthorized + 'anything25' | :unauthorized + end + + with_them do + it_behaves_like 'rejects nuget packages access', :anonymous, params[:status] + end + end +end + +RSpec.shared_examples 'rejects nuget access with unknown project id' do + context 'with an unknown project' do + let(:project) { OpenStruct.new(id: 1234567890) } + + context 'as anonymous' do + it_behaves_like 'rejects nuget packages access', :anonymous, :unauthorized + end + + context 'as authenticated user' do + subject { get api(url), headers: build_basic_auth_header(user.username, personal_access_token.token) } + + it_behaves_like 'rejects nuget packages access', :anonymous, :not_found + end + end +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 new file mode 100644 index 00000000000..ec15d7a4d2e --- /dev/null +++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +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) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + context 'valid token' do + it_behaves_like 'returning response status', :success + end + + context 'invalid token' do + let(:headers) { build_basic_auth_header(deploy_token.username, 'bar') } + + it_behaves_like 'returning response status', :unauthorized + end + end +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) } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + context 'valid token' do + it_behaves_like 'returning response status', :success + end + + context 'invalid token' do + let(:headers) { build_basic_auth_header(deploy_token.username, 'bar').merge(workhorse_header) } + + it_behaves_like 'returning response status', :unauthorized + end + end +end diff --git a/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb new file mode 100644 index 00000000000..a371d380f47 --- /dev/null +++ b/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb @@ -0,0 +1,185 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'rejects package tags access' do |user_type, status| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) unless user_type == :no_type + end + + it_behaves_like 'returning response status', status + end +end + +RSpec.shared_examples 'returns package tags' do |user_type| + using RSpec::Parameterized::TableSyntax + + before do + stub_application_setting(npm_package_requests_forwarding: false) + project.send("add_#{user_type}", user) unless user_type == :no_type + end + + it_behaves_like 'returning response status', :success + + it 'returns a valid json response' do + subject + + expect(response.media_type).to eq('application/json') + expect(json_response).to be_a(Hash) + end + + it 'returns two package tags' do + subject + + expect(json_response).to match_schema('public_api/v4/packages/npm_package_tags') + expect(json_response.length).to eq(3) # two tags + latest (auto added) + expect(json_response[package_tag1.name]).to eq(package.version) + expect(json_response[package_tag2.name]).to eq(package.version) + expect(json_response['latest']).to eq(package.version) + end + + context 'with invalid package name' do + where(:package_name, :status) do + '%20' | :bad_request + nil | :forbidden + end + + with_them do + it_behaves_like 'returning response status', params[:status] + end + end +end + +RSpec.shared_examples 'create package tag' do |user_type| + using RSpec::Parameterized::TableSyntax + + before do + project.send("add_#{user_type}", user) unless user_type == :no_type + end + + it_behaves_like 'returning response status', :no_content + + it 'creates the package tag' do + expect { subject }.to change { Packages::Tag.count }.by(1) + + last_tag = Packages::Tag.last + expect(last_tag.name).to eq(tag_name) + expect(last_tag.package).to eq(package) + end + + it 'returns a valid response' do + subject + + expect(response.body).to be_empty + end + + context 'with already existing tag' do + let_it_be(:package2) { create(:npm_package, project: project, name: package.name, version: '5.5.55') } + let_it_be(:tag) { create(:packages_tag, package: package2, name: tag_name) } + + it_behaves_like 'returning response status', :no_content + + it 'reuses existing tag' do + expect(package.tags).to be_empty + expect(package2.tags).to eq([tag]) + expect { subject }.to not_change { Packages::Tag.count } + expect(package.reload.tags).to eq([tag]) + expect(package2.reload.tags).to be_empty + end + + it 'returns a valid response' do + subject + + expect(response.body).to be_empty + end + end + + context 'with invalid package name' do + where(:package_name, :status) do + 'unknown' | :forbidden + '' | :not_found + '%20' | :bad_request + end + + with_them do + it_behaves_like 'returning response status', params[:status] + end + end + + context 'with invalid tag name' do + where(:tag_name, :status) do + '' | :not_found + '%20' | :bad_request + end + + with_them do + it_behaves_like 'returning response status', params[:status] + end + end + + context 'with invalid version' do + where(:version, :status) do + ' ' | :bad_request + '' | :bad_request + nil | :bad_request + end + + with_them do + it_behaves_like 'returning response status', params[:status] + end + end +end + +RSpec.shared_examples 'delete package tag' do |user_type| + using RSpec::Parameterized::TableSyntax + + before do + project.send("add_#{user_type}", user) unless user_type == :no_type + end + + context "for #{user_type} user" do + it_behaves_like 'returning response status', :no_content + + it 'returns a valid response' do + subject + + expect(response.body).to be_empty + end + + it 'destroy the package tag' do + expect(package.tags).to eq([package_tag]) + expect { subject }.to change { Packages::Tag.count }.by(-1) + expect(package.reload.tags).to be_empty + end + + context 'with tag from other package' do + let(:package2) { create(:npm_package, project: project) } + let(:package_tag) { create(:packages_tag, package: package2) } + + it_behaves_like 'returning response status', :not_found + end + + context 'with invalid package name' do + where(:package_name, :status) do + 'unknown' | :forbidden + '' | :not_found + '%20' | :bad_request + end + + with_them do + it_behaves_like 'returning response status', params[:status] + end + end + + context 'with invalid tag name' do + where(:tag_name, :status) do + 'unknown' | :not_found + '' | :not_found + '%20' | :bad_request + end + + with_them do + it_behaves_like 'returning response status', params[:status] + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb new file mode 100644 index 00000000000..fcc166ac87d --- /dev/null +++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'PyPi package creation' do |user_type, status, add_member = true| + RSpec.shared_examples 'creating pypi package files' do + it 'creates package files' do + expect { subject } + .to change { project.packages.pypi.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + .and change { Packages::Pypi::Metadatum.count }.by(1) + expect(response).to have_gitlab_http_status(status) + + package = project.reload.packages.pypi.last + + expect(package.name).to eq params[:name] + expect(package.version).to eq params[:version] + expect(package.pypi_metadatum.required_python).to eq params[:requires_python] + end + end + + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'creating pypi package files' + + context 'with object storage disabled' do + before do + stub_package_file_object_storage(enabled: false) + end + + context 'without a file from workhorse' do + let(:send_rewritten_field) { false } + + it_behaves_like 'returning response status', :bad_request + end + + context 'with correct params' do + it_behaves_like 'package workhorse uploads' + it_behaves_like 'creating pypi package files' + it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package' + end + end + + context 'with object storage enabled' do + let(:tmp_object) do + fog_connection.directories.new(key: 'packages').files.create( + key: "tmp/uploads/#{file_name}", + 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) } + + context 'and direct upload enabled' do + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: true) + end + + it_behaves_like 'creating pypi package files' + + ['123123', '../../123123'].each do |remote_id| + context "with invalid remote_id: #{remote_id}" do + let(:params) { base_params.merge(content: fog_file, 'content.remote_id' => remote_id) } + + it_behaves_like 'returning response status', :forbidden + end + end + end + + context 'and direct upload disabled' do + context 'and background upload disabled' do + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: false, background_upload: false) + end + + it_behaves_like 'creating pypi package files' + end + + context 'and background upload enabled' do + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: false, background_upload: true) + end + + it_behaves_like 'creating pypi package files' + end + end + end + + it_behaves_like 'background upload schedules a file migration' + end +end + +RSpec.shared_examples 'PyPi package versions' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it 'returns the package listing' do + subject + + expect(response.body).to match(package.package_files.first.file_name) + end + + it_behaves_like 'returning response status', status + it_behaves_like 'a gitlab tracking event', described_class.name, 'list_package' + end +end + +RSpec.shared_examples 'PyPi package download' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it 'returns the package listing' do + subject + + expect(response.body).to eq(File.open(package.package_files.first.file.path, "rb").read) + end + + it_behaves_like 'returning response status', status + it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package' + end +end + +RSpec.shared_examples 'process PyPi api request' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + end +end + +RSpec.shared_examples 'rejects PyPI access with unknown project id' do + context 'with an unknown project' do + let(:project) { OpenStruct.new(id: 1234567890) } + + context 'as anonymous' do + it_behaves_like 'process PyPi api request', :anonymous, :not_found + end + + context 'as authenticated user' do + subject { get api(url), headers: build_basic_auth_header(user.username, personal_access_token.token) } + + it_behaves_like 'process PyPi api request', :anonymous, :not_found + end + end +end diff --git a/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb b/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb index bca51dab353..d21a9f419fd 100644 --- a/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb @@ -16,6 +16,29 @@ RSpec.shared_examples 'resource_milestone_events API' do |parent_type, eventable expect(json_response.first['action']).to eq(event.action) end + context 'when there is an event with a milestone which is not visible for requesting user' do + let!(:private_project) { create(:project, :private) } + let!(:private_milestone) { create(:milestone, project: private_project) } + + let!(:other_user) { create(:user) } + + it 'returns the expected events' do + create_event(private_milestone) + + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events", other_user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq('1') + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + + expect(json_response.first['id']).to eq(event.id) + expect(json_response.first['milestone']['id']).to eq(event.milestone.id) + expect(json_response.first['action']).to eq(event.action) + end + end + it "returns a 404 error when eventable id not found" do get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{non_existing_record_id}/resource_milestone_events", user) @@ -60,6 +83,20 @@ RSpec.shared_examples 'resource_milestone_events API' do |parent_type, eventable end end + describe 'pagination' do + let!(:event1) { create_event(milestone) } + let!(:event2) { create_event(milestone) } + + # https://gitlab.com/gitlab-org/gitlab/-/issues/220192 + it 'returns the second page' do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events?page=2&per_page=1", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq(1) + expect(json_response.first['id']).to eq(event2.id) + end + end + def create_event(milestone, action: :add) create(:resource_milestone_event, eventable.class.name.underscore => eventable, milestone: milestone, action: action) end diff --git a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb new file mode 100644 index 00000000000..cfbb84dd099 --- /dev/null +++ b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'raw snippet files' do + let_it_be(:unauthorized_user) { create(:user) } + let(:snippet_id) { snippet.id } + let(:user) { snippet.author } + let(:file_path) { '%2Egitattributes' } + let(:ref) { 'master' } + + context 'with no user' do + it 'requires authentication' do + get api(api_path) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + shared_examples 'not found' do + it 'returns 404' do + get api(api_path, user) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + end + + context 'when not authorized' do + let(:user) { unauthorized_user } + + it_behaves_like 'not found' + end + + context 'with an invalid snippet ID' do + let(:snippet_id) { 'invalid' } + + it_behaves_like 'not found' + end + + context 'with valid params' do + it 'returns the raw file info' do + expect(Gitlab::Workhorse).to receive(:send_git_blob).and_call_original + + get api(api_path, user) + + aggregate_failures do + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq 'text/plain' + expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true' + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') + expect(response.header['Content-Disposition']).to match 'filename=".gitattributes"' + end + end + end + + context 'with invalid params' do + using RSpec::Parameterized::TableSyntax + + where(:file_path, :ref, :status, :key, :message) do + '%2Egitattributes' | 'invalid-ref' | :not_found | 'message' | '404 Reference Not Found' + '%2Egitattributes' | nil | :not_found | 'error' | '404 Not Found' + '%2Egitattributes' | '' | :not_found | 'error' | '404 Not Found' + + 'doesnotexist.rb' | 'master' | :not_found | 'message' | '404 File Not Found' + '/does/not/exist.rb' | 'master' | :not_found | 'error' | '404 Not Found' + '%2E%2E%2Fetc%2Fpasswd' | 'master' | :bad_request | 'error' | 'file_path should be a valid file path' + '%2Fetc%2Fpasswd' | 'master' | :bad_request | 'error' | 'file_path should be a valid file path' + '../../etc/passwd' | 'master' | :not_found | 'error' | '404 Not Found' + end + + with_them do + before do + get api(api_path, user) + end + + it { expect(response).to have_gitlab_http_status(status) } + it { expect(json_response[key]).to eq(message) } + end + end +end diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb index f830f957174..644abb191a6 100644 --- a/spec/support/shared_examples/requests/snippet_shared_examples.rb +++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb @@ -74,18 +74,14 @@ RSpec.shared_examples 'update with repository actions' do end end -RSpec.shared_examples 'snippet response without repository URLs' do - it 'skip inclusion of repository URLs' do - expect(json_response).not_to have_key('ssh_url_to_repo') - expect(json_response).not_to have_key('http_url_to_repo') - end -end - RSpec.shared_examples 'snippet blob content' do it 'returns content from repository' do + expect(Gitlab::Workhorse).to receive(:send_git_blob).and_call_original + subject - expect(response.body).to eq(snippet.blobs.first.data) + expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true' + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') end context 'when snippet repository is empty' do @@ -98,3 +94,15 @@ RSpec.shared_examples 'snippet blob content' do end end end + +RSpec.shared_examples 'snippet_multiple_files feature disabled' do + before do + stub_feature_flags(snippet_multiple_files: false) + + subject + end + + it 'does not return files attributes' do + expect(json_response).not_to have_key('files') + end +end diff --git a/spec/support/shared_examples/resource_events.rb b/spec/support/shared_examples/resource_events.rb index 66f5e760c37..c0158f9b24b 100644 --- a/spec/support/shared_examples/resource_events.rb +++ b/spec/support/shared_examples/resource_events.rb @@ -2,7 +2,7 @@ require 'spec_helper' -shared_examples 'a resource event' do +RSpec.shared_examples 'a resource event' do let_it_be(:user1) { create(:user) } let_it_be(:user2) { create(:user) } @@ -54,7 +54,7 @@ shared_examples 'a resource event' do end end -shared_examples 'a resource event for issues' do +RSpec.shared_examples 'a resource event for issues' do let_it_be(:user1) { create(:user) } let_it_be(:user2) { create(:user) } @@ -101,9 +101,19 @@ shared_examples 'a resource event for issues' do expect(events).to be_empty end end + + if described_class.method_defined?(:issuable) + describe '#issuable' do + let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue2) } + + it 'returns the expected issuable' do + expect(event1.issuable).to eq(issue2) + end + end + end end -shared_examples 'a resource event for merge requests' do +RSpec.shared_examples 'a resource event for merge requests' do let_it_be(:user1) { create(:user) } let_it_be(:user2) { create(:user) } @@ -132,4 +142,14 @@ shared_examples 'a resource event for merge requests' do expect(events).to be_empty end end + + if described_class.method_defined?(:issuable) + describe '#issuable' do + let_it_be(:event1) { create(described_class.name.underscore.to_sym, merge_request: merge_request2) } + + it 'returns the expected issuable' do + expect(event1.issuable).to eq(merge_request2) + end + end + end end diff --git a/spec/support/shared_examples/routing/resource_routing_shared_examples.rb b/spec/support/shared_examples/routing/resource_routing_shared_examples.rb new file mode 100644 index 00000000000..b98901a57ea --- /dev/null +++ b/spec/support/shared_examples/routing/resource_routing_shared_examples.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +# Shared examples for resource routes. +# +# By default it tests all the default REST actions: index, create, new, edit, +# show, update, and destroy. You can remove actions by customizing the +# `actions` variable. +# +# The subject is expected to be an instance of the controller under test. +# +# It also expects a `base_path` variable to be available which defines the +# base path of the controller, and a `base_params` variable which +# defines the route params the base path maps to. +# +# Examples +# +# # Default behavior +# describe Projects::CommitsController, 'routing' do +# it_behaves_like 'resource routing' do +# let(:base_path) { '/gitlab/gitlabhq/-/commits' } +# let(:base_params) { { namespace_id: 'gitlab', project_id: 'gitlabhq' } } +# end +# end +# +# # Customizing actions +# it_behaves_like 'resource routing' do +# let(:base_path) { '/gitlab/gitlabhq/-/commits' } +# +# # Specify default actions +# let(:actions) { [:index] } +# +# # Add custom actions by passing a hash with action names +# # as keys, and the HTTP method and path as values. +# let(:additional_actions) do +# { +# preview_markdown: [:post, '/:id/preview_markdown'], +# } +# end +# end +RSpec.shared_examples 'resource routing' do + let(:controller) { described_class.controller_path } + let(:id) { '123' } + + let(:default_actions) do + { + index: [:get, ''], + show: [:get, '/:id'], + new: [:get, '/new'], + create: [:post, ''], + edit: [:get, '/:id/edit'], + update: [:put, '/:id'], + destroy: [:delete, '/:id'] + } + end + + let(:actions) { default_actions.keys } + let(:additional_actions) { {} } + + it 'routes resource actions', :aggregate_failures do + selected_actions = default_actions.slice(*actions).merge(additional_actions) + + selected_actions.each do |action, (method, action_path)| + expected_params = base_params.merge(controller: controller.to_s, action: action.to_s) + + if action_path.include?(':id') + action_path = action_path.sub(':id', id) + expected_params[:id] = id + end + + expect(public_send(method, "#{base_path}#{action_path}")).to route_to(expected_params) + end + end +end diff --git a/spec/support/shared_examples/routing/wiki_routing_shared_examples.rb b/spec/support/shared_examples/routing/wiki_routing_shared_examples.rb new file mode 100644 index 00000000000..9289934677e --- /dev/null +++ b/spec/support/shared_examples/routing/wiki_routing_shared_examples.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'wiki routing' do + it_behaves_like 'resource routing' do + let(:id) { 'directory/page' } + let(:actions) { %i[show new create edit update destroy] } + let(:additional_actions) do + { + pages: [:get, '/pages'], + history: [:get, '/:id/history'], + git_access: [:get, '/git_access'], + preview_markdown: [:post, '/:id/preview_markdown'] + } + end + end + + it 'redirects the base path to the home page', type: :request do + expect(get(base_path)).to redirect_to("#{base_path}/home") + end +end diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb new file mode 100644 index 00000000000..a1354a8099b --- /dev/null +++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'creates an alert management alert' do + it { is_expected.to be_success } + + it 'creates AlertManagement::Alert' do + expect { subject }.to change(AlertManagement::Alert, :count).by(1) + end + + it 'executes the alert service hooks' do + slack_service = create(:service, type: 'SlackService', project: project, alert_events: true, active: true) + + subject + + expect(ProjectServiceWorker).to have_received(:perform_async).with(slack_service.id, an_instance_of(Hash)) + end +end + +RSpec.shared_examples 'does not an create alert management alert' do + it 'does not create alert' do + expect { subject }.not_to change(AlertManagement::Alert, :count) + end +end + +RSpec.shared_examples 'adds an alert management alert event' do + it { is_expected.to be_success } + + it 'does not create an alert' do + expect { subject }.not_to change(AlertManagement::Alert, :count) + end + + it 'increases alert events count' do + expect { subject }.to change { alert.reload.events }.by(1) + end + + it 'does not executes the alert service hooks' do + expect(alert).not_to receive(:execute_services) + + subject + end +end diff --git a/spec/support/shared_examples/services/clusters/parse_cluster_applications_artifact_shared_examples.rb b/spec/support/shared_examples/services/clusters/parse_cluster_applications_artifact_shared_examples.rb new file mode 100644 index 00000000000..cbe20928f98 --- /dev/null +++ b/spec/support/shared_examples/services/clusters/parse_cluster_applications_artifact_shared_examples.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'parse cluster applications artifact' do |release_name| + let(:application_class) { Clusters::Cluster::APPLICATIONS[release_name] } + let(:cluster_application) { cluster.public_send("application_#{release_name}") } + let(:file) { fixture_file_upload(Rails.root.join(fixture)) } + let(:artifact) { create(:ci_job_artifact, :cluster_applications, job: job, file: file) } + + context 'release is missing' do + let(:fixture) { "spec/fixtures/helm/helm_list_v2_#{release_name}_missing.json.gz" } + + context 'application does not exist' do + it 'does not create or destroy an application' do + expect do + described_class.new(job, user).execute(artifact) + end.not_to change(application_class, :count) + end + end + + context 'application exists' do + before do + create("clusters_applications_#{release_name}".to_sym, :installed, cluster: cluster) + end + + it 'marks the application as uninstalled' do + described_class.new(job, user).execute(artifact) + + cluster_application.reload + expect(cluster_application).to be_uninstalled + end + end + end + + context 'release is deployed' do + let(:fixture) { "spec/fixtures/helm/helm_list_v2_#{release_name}_deployed.json.gz" } + + context 'application does not exist' do + it 'creates an application and marks it as installed' do + expect do + described_class.new(job, user).execute(artifact) + end.to change(application_class, :count) + + expect(cluster_application).to be_persisted + expect(cluster_application).to be_installed + end + end + + context 'application exists' do + before do + create("clusters_applications_#{release_name}".to_sym, :errored, cluster: cluster) + end + + it 'marks the application as installed' do + described_class.new(job, user).execute(artifact) + + expect(cluster_application).to be_installed + end + end + end + + context 'release is failed' do + let(:fixture) { "spec/fixtures/helm/helm_list_v2_#{release_name}_failed.json.gz" } + + context 'application does not exist' do + it 'creates an application and marks it as errored' do + expect do + described_class.new(job, user).execute(artifact) + end.to change(application_class, :count) + + expect(cluster_application).to be_persisted + expect(cluster_application).to be_errored + expect(cluster_application.status_reason).to eq('Helm release failed to install') + end + end + + context 'application exists' do + before do + create("clusters_applications_#{release_name}".to_sym, :installed, cluster: cluster) + end + + it 'marks the application as errored' do + described_class.new(job, user).execute(artifact) + + expect(cluster_application).to be_errored + expect(cluster_application.status_reason).to eq('Helm release failed to install') + end + end + end +end diff --git a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb index 4ce3e32d774..20856b05de6 100644 --- a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb +++ b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb @@ -17,10 +17,10 @@ RSpec.shared_examples 'system note creation' do |update_params, note_text| end end -RSpec.shared_examples 'WIP notes creation' do |wip_action| +RSpec.shared_examples 'draft notes creation' do |wip_action| subject { described_class.new(project, user).execute(issuable, old_labels: []) } - it 'creates WIP toggle and title change notes' do + it 'creates Draft toggle and title change notes' do expect { subject }.to change { Note.count }.from(0).to(2) expect(Note.first.note).to match("#{wip_action} as a **Work In Progress**") diff --git a/spec/support/shared_examples/services/jira_import/start_import_service_shared_examples.rb b/spec/support/shared_examples/services/jira_import/start_import_service_shared_examples.rb index c5e56ed3539..8fd76f7cb1f 100644 --- a/spec/support/shared_examples/services/jira_import/start_import_service_shared_examples.rb +++ b/spec/support/shared_examples/services/jira_import/start_import_service_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'responds with error' do |message| +RSpec.shared_examples 'responds with error' do |message| it 'returns error' do expect(subject).to be_a(ServiceResponse) expect(subject).to be_error 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 5dd1badbefc..c8fabfe30b9 100644 --- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb +++ b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb @@ -29,14 +29,43 @@ RSpec.shared_examples 'valid dashboard service response' do end RSpec.shared_examples 'caches the unprocessed dashboard for subsequent calls' do - it do - expect(YAML).to receive(:safe_load).once.and_call_original + specify do + expect_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader| + expect(loader).to receive(:load_raw!).once.and_call_original + end described_class.new(*service_params).get_dashboard described_class.new(*service_params).get_dashboard end end +# This spec is applicable for predefined/out-of-the-box dashboard services. +RSpec.shared_examples 'refreshes cache when dashboard_version is changed' do + specify do + allow_next_instance_of(described_class) do |service| + allow(service).to receive(:dashboard_version).and_return('1', '2') + end + + expect(File).to receive(:read).twice.and_call_original + + service = described_class.new(*service_params) + + service.get_dashboard + service.get_dashboard + end +end + +# This spec is applicable for predefined/out-of-the-box dashboard services. +# This shared_example requires the following variables to be defined: +# dashboard_path: Relative path to the dashboard, ex: 'config/prometheus/common_metrics.yml' +# dashboard_version: The version string used in the cache_key. +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) + end +end + RSpec.shared_examples 'valid embedded dashboard service response' do let(:dashboard_schema) { Gitlab::Json.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json')) } @@ -128,3 +157,50 @@ RSpec.shared_examples 'updates gitlab_metrics_dashboard_processing_time_ms metri expect(metric.get(labels)).to be > 0 end end + +RSpec.shared_examples '#raw_dashboard raises error if dashboard loading fails' do + context 'when yaml is too large' do + before do + allow_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader| + allow(loader).to receive(:load_raw!) + .and_raise(Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big') + end + end + + it 'raises error' do + expect { subject.raw_dashboard }.to raise_error( + Gitlab::Metrics::Dashboard::Errors::LayoutError, + 'The parsed YAML is too big' + ) + end + end + + context 'when yaml loader returns error' do + before do + allow_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader| + allow(loader).to receive(:load_raw!) + .and_raise(Gitlab::Config::Loader::FormatError, 'Invalid configuration format') + end + end + + it 'raises error' do + expect { subject.raw_dashboard }.to raise_error( + Gitlab::Metrics::Dashboard::Errors::LayoutError, + 'Invalid yaml' + ) + end + end + + context 'when yaml is not a hash' do + before do + allow_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader| + allow(loader).to receive(:load_raw!) + .and_raise(Gitlab::Config::Loader::Yaml::NotHashError, 'Invalid configuration format') + end + end + + it 'returns nil' do + expect(subject.raw_dashboard).to eq({}) + end + end +end diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb new file mode 100644 index 00000000000..45a4c2bb151 --- /dev/null +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -0,0 +1,193 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'assigns build to package' do + context 'with build info' do + let(:job) { create(:ci_build, user: user) } + let(:params) { super().merge(build: job) } + + it 'assigns the pipeline to the package' do + package = subject + + expect(package.build_info).to be_present + expect(package.build_info.pipeline).to eq job.pipeline + end + end +end + +RSpec.shared_examples 'returns packages' do |container_type, user_type| + context "for #{user_type}" do + before do + send(container_type)&.send("add_#{user_type}", user) unless user_type == :no_type + end + + it 'returns success response' do + subject + + expect(response).to have_gitlab_http_status(:success) + end + + it 'returns a valid response schema' do + subject + + expect(response).to match_response_schema(package_schema) + end + + it 'returns two packages' do + subject + + expect(json_response.length).to eq(2) + expect(json_response.map { |package| package['id'] }).to contain_exactly(package1.id, package2.id) + end + end +end + +RSpec.shared_examples 'returns packages with subgroups' do |container_type, user_type| + context "with subgroups for #{user_type}" do + before do + send(container_type)&.send("add_#{user_type}", user) unless user_type == :no_type + end + + it 'returns success response' do + subject + + expect(response).to have_gitlab_http_status(:success) + end + + it 'returns a valid response schema' do + subject + + expect(response).to match_response_schema(package_schema) + end + + it 'returns three packages' do + subject + + expect(json_response.length).to eq(3) + expect(json_response.map { |package| package['id'] }).to contain_exactly(package1.id, package2.id, package3.id) + end + end +end + +RSpec.shared_examples 'package sorting' do |order_by| + subject { get api(url), params: { sort: sort, order_by: order_by } } + + context "sorting by #{order_by}" do + context 'ascending order' do + let(:sort) { 'asc' } + + it 'returns the sorted packages' do + subject + + expect(json_response.map { |package| package['id'] }).to eq(packages.map(&:id)) + end + end + + context 'descending order' do + let(:sort) { 'desc' } + + it 'returns the sorted packages' do + subject + + expect(json_response.map { |package| package['id'] }).to eq(packages.reverse.map(&:id)) + end + end + end +end + +RSpec.shared_examples 'rejects packages access' do |container_type, user_type, status| + context "for #{user_type}" do + before do + send(container_type)&.send("add_#{user_type}", user) unless user_type == :no_type + end + + it_behaves_like 'returning response status', status + end +end + +RSpec.shared_examples 'returns paginated packages' do + let(:per_page) { 2 } + + context 'when viewing the first page' do + let(:page) { 1 } + + it 'returns first 2 packages' do + get api(url, user), params: { page: page, per_page: per_page } + + expect_paginated_array_response([package1.id, package2.id]) + end + end + + context 'when viewing the second page' do + let(:page) { 2 } + + it 'returns first 2 packages' do + get api(url, user), params: { page: page, per_page: per_page } + + expect_paginated_array_response([package3.id, package4.id]) + end + end +end + +RSpec.shared_examples 'background upload schedules a file migration' do + context 'background upload enabled' do + before do + stub_package_file_object_storage(background_upload: true) + end + + it 'schedules migration of file to object storage' do + expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('Packages::PackageFileUploader', 'Packages::PackageFile', :file, kind_of(Numeric)) + + subject + end + end +end + +RSpec.shared_context 'package filter context' do + def package_filter_url(filter, param) + "/projects/#{project.id}/packages?package_#{filter}=#{param}" + end + + def group_filter_url(filter, param) + "/groups/#{group.id}/packages?package_#{filter}=#{param}" + end +end + +RSpec.shared_examples 'filters on each package_type' do |is_project: false| + include_context 'package filter context' + + let_it_be(:package1) { create(:conan_package, project: project) } + let_it_be(:package2) { create(:maven_package, project: project) } + let_it_be(:package3) { create(:npm_package, project: project) } + let_it_be(:package4) { create(:nuget_package, project: project) } + let_it_be(:package5) { create(:pypi_package, project: project) } + let_it_be(:package6) { create(:composer_package, project: project) } + + Packages::Package.package_types.keys.each do |package_type| + context "for package type #{package_type}" do + let(:url) { is_project ? package_filter_url(:type, package_type) : group_filter_url(:type, package_type) } + + subject { get api(url, user) } + + it "returns #{package_type} packages" do + subject + + expect(json_response.length).to eq(1) + expect(json_response.map { |package| package['package_type'] }).to contain_exactly(package_type) + end + end + end +end + +RSpec.shared_examples 'package workhorse uploads' do + context 'without a workhorse header' do + let(:workhorse_token) { JWT.encode({ 'iss' => 'invalid header' }, Gitlab::Workhorse.secret, 'HS256') } + + it_behaves_like 'returning response status', :forbidden + + it 'logs an error' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).once + + subject + end + end +end 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 0e6ecf49cd0..2ddbdebdb97 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 @@ -25,19 +25,18 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| 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(:create_repository) - .and_return(true) allow(project_repository_double).to receive(:replicate) .with(project.repository.raw) allow(project_repository_double).to receive(:checksum) .and_return(project_repository_checksum) - allow(repository_double).to receive(:create_repository) - .and_return(true) allow(repository_double).to receive(:replicate) .with(repository.raw) 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 end it "moves the project and its #{repository_type} repository to the new storage and unmarks the repository as read only" do @@ -48,6 +47,7 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| old_repository_path = repository.full_path result = subject.execute + project.reload expect(result).to be_success expect(project).not_to be_repository_read_only @@ -101,15 +101,11 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| it 'unmarks the repository as read-only without updating the repository storage' 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(:create_repository) - .and_return(true) allow(project_repository_double).to receive(:replicate) .with(project.repository.raw) allow(project_repository_double).to receive(:checksum) .and_return(project_repository_checksum) - allow(repository_double).to receive(:create_repository) - .and_return(true) allow(repository_double).to receive(:replicate) .with(repository.raw) .and_raise(Gitlab::Git::CommandError) @@ -128,15 +124,11 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| it 'unmarks the repository as read-only without updating the repository storage' 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(:create_repository) - .and_return(true) allow(project_repository_double).to receive(:replicate) .with(project.repository.raw) allow(project_repository_double).to receive(:checksum) .and_return(project_repository_checksum) - allow(repository_double).to receive(:create_repository) - .and_return(true) allow(repository_double).to receive(:replicate) .with(repository.raw) allow(repository_double).to receive(:checksum) 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 c5f84e205cf..ef41c2fcc13 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,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'a milestone events creator' do +RSpec.shared_examples 'a milestone events creator' do let_it_be(:user) { create(:user) } let(:created_at_time) { Time.utc(2019, 12, 30) } 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 efcb83a34af..ebe78c299a5 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 @@ -63,16 +63,6 @@ RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type| include_examples 'correct event created' end - context 'the feature is disabled' do - before do - stub_feature_flags(wiki_events: false) - end - - it 'does not record the activity' do - expect { service.execute }.not_to change(Event, :count) - end - end - context 'when the options are bad' do let(:page_title) { '' } diff --git a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb index 1231c012c31..db1b50fdf3c 100644 --- a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb +++ b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb @@ -37,14 +37,4 @@ RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type| expect { service.execute(nil) }.not_to change { counter.read(:delete) } end - - context 'the feature is disabled' do - before do - stub_feature_flags(wiki_events: false) - end - - it 'does not record the activity' do - expect { service.execute(page) }.not_to change(Event, :count) - end - end end diff --git a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb index 77354fec069..0191a6dfbc9 100644 --- a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb +++ b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb @@ -67,16 +67,6 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type| include_examples 'adds activity event' end - context 'the feature is disabled' do - before do - stub_feature_flags(wiki_events: false) - end - - it 'does not record the activity' do - expect { service.execute(page) }.not_to change(Event, :count) - end - end - context 'when the options are bad' do let(:page_title) { '' } diff --git a/spec/support/shared_examples/snippet_blob_shared_examples.rb b/spec/support/shared_examples/snippet_blob_shared_examples.rb new file mode 100644 index 00000000000..ba97688d017 --- /dev/null +++ b/spec/support/shared_examples/snippet_blob_shared_examples.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'snippet blob raw path' 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 path' do + expect(subject).to eq("/-/snippets/#{snippet.id}/raw/#{ref}/#{blob.path}") + end + end + + context 'for ProjectSnippets' do + let(:snippet) { project_snippet } + + it 'returns the raw project snippet blob path' do + expect(subject).to eq("/#{snippet.project.full_path}/-/snippets/#{snippet.id}/raw/#{ref}/#{blob.path}") + end + end +end diff --git a/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb b/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb index e58723324d3..9e0104e1410 100644 --- a/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb +++ b/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb @@ -2,7 +2,7 @@ # @param path [String] the path to file to upload. E.g. File.join('spec', 'fixtures', 'sanitized.svg') # @param uploader [CarrierWave::Uploader::Base] uploader to handle the upload. -shared_examples 'denied carrierwave upload' do +RSpec.shared_examples 'denied carrierwave upload' do it 'will deny upload' do fixture_file = fixture_file_upload(path) expect { uploader.cache!(fixture_file) }.to raise_exception(CarrierWave::IntegrityError) @@ -11,7 +11,7 @@ end # @param path [String] the path to file to upload. E.g. File.join('spec', 'fixtures', 'sanitized.svg') # @param uploader [CarrierWave::Uploader::Base] uploader to handle the upload. -shared_examples 'accepted carrierwave upload' do +RSpec.shared_examples 'accepted carrierwave upload' do let(:fixture_file) { fixture_file_upload(path) } before do @@ -30,7 +30,7 @@ end # @param path [String] the path to file to upload. E.g. File.join('spec', 'fixtures', 'sanitized.svg') # @param uploader [CarrierWave::Uploader::Base] uploader to handle the upload. # @param content_type [String] the upload file content type after cache -shared_examples 'upload with content type' do |content_type| +RSpec.shared_examples 'upload with content type' do |content_type| let(:fixture_file) { fixture_file_upload(path, content_type) } it 'will not change upload file content type' do diff --git a/spec/support/shared_examples/views/pipeline_status_changes_email.rb b/spec/support/shared_examples/views/pipeline_status_changes_email.rb index 15b4ce9c44e..698f11c2216 100644 --- a/spec/support/shared_examples/views/pipeline_status_changes_email.rb +++ b/spec/support/shared_examples/views/pipeline_status_changes_email.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'pipeline status changes email' do +RSpec.shared_examples 'pipeline status changes email' do include Devise::Test::ControllerHelpers let(:user) { create(:user, developer_projects: [project]) } diff --git a/spec/support/shared_examples/views/plain_text_email.rb b/spec/support/shared_examples/views/plain_text_email.rb new file mode 100644 index 00000000000..23f9262b446 --- /dev/null +++ b/spec/support/shared_examples/views/plain_text_email.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'renders plain text email correctly' do + it 'renders the email without HTML links' do + render + + expect(rendered).to have_no_selector('a') + end +end diff --git a/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb b/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb index ae8c82cb67c..2cca76c8fe3 100644 --- a/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb +++ b/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'include import workers modules' do +RSpec.shared_examples 'include import workers modules' do it { expect(described_class).to include_module(ApplicationWorker) } it { expect(described_class).to include_module(Gitlab::JiraImport::QueueOptions) } @@ -12,7 +12,7 @@ shared_examples 'include import workers modules' do end end -shared_examples 'does not advance to next stage' do +RSpec.shared_examples 'does not advance to next stage' do it 'does not advance to next stage' do expect(Gitlab::JiraImport::AdvanceStageWorker).not_to receive(:perform_async) @@ -20,7 +20,7 @@ shared_examples 'does not advance to next stage' do end end -shared_examples 'cannot do Jira import' do +RSpec.shared_examples 'cannot do Jira import' do it 'does not advance to next stage' do worker = described_class.new expect(worker).not_to receive(:import) @@ -29,7 +29,7 @@ shared_examples 'cannot do Jira import' do end end -shared_examples 'advance to next stage' do |next_stage| +RSpec.shared_examples 'advance to next stage' do |next_stage| let(:job_waiter) { Gitlab::JobWaiter.new(2, 'some-job-key') } it "advances to #{next_stage} stage" do |