diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-20 08:43:02 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-20 08:43:02 +0000 |
commit | d9ab72d6080f594d0b3cae15f14b3ef2c6c638cb (patch) | |
tree | 2341ef426af70ad1e289c38036737e04b0aa5007 /spec/requests/api | |
parent | d6e514dd13db8947884cd58fe2a9c2a063400a9b (diff) | |
download | gitlab-ce-d9ab72d6080f594d0b3cae15f14b3ef2c6c638cb.tar.gz |
Add latest changes from gitlab-org/gitlab@14-4-stable-eev14.4.0-rc42
Diffstat (limited to 'spec/requests/api')
57 files changed, 1971 insertions, 680 deletions
diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb index 81620fce448..95eb503c6bc 100644 --- a/spec/requests/api/api_spec.rb +++ b/spec/requests/api/api_spec.rb @@ -100,39 +100,105 @@ RSpec.describe API::API do end end - context 'application context' do - let_it_be(:project) { create(:project) } + describe 'logging', :aggregate_failures do + let_it_be(:project) { create(:project, :public) } let_it_be(:user) { project.owner } - it 'logs all application context fields' do - allow_any_instance_of(Gitlab::GrapeLogging::Loggers::ContextLogger).to receive(:parameters) do - Gitlab::ApplicationContext.current.tap do |log_context| - expect(log_context).to match('correlation_id' => an_instance_of(String), - 'meta.caller_id' => 'GET /api/:version/projects/:id/issues', - 'meta.remote_ip' => an_instance_of(String), - 'meta.project' => project.full_path, - 'meta.root_namespace' => project.namespace.full_path, - 'meta.user' => user.username, - 'meta.client_id' => an_instance_of(String), - 'meta.feature_category' => 'issue_tracking') + context 'when the endpoint is handled by the application' do + context 'when the endpoint supports all possible fields' do + it 'logs all application context fields and the route' do + expect(described_class::LOG_FORMATTER).to receive(:call) do |_severity, _datetime, _, data| + expect(data.stringify_keys) + .to include('correlation_id' => an_instance_of(String), + 'meta.caller_id' => 'GET /api/:version/projects/:id/issues', + 'meta.remote_ip' => an_instance_of(String), + 'meta.project' => project.full_path, + 'meta.root_namespace' => project.namespace.full_path, + 'meta.user' => user.username, + 'meta.client_id' => a_string_matching(%r{\Auser/.+}), + 'meta.feature_category' => 'issue_tracking', + 'route' => '/api/:version/projects/:id/issues') + end + + get(api("/projects/#{project.id}/issues", user)) + + expect(response).to have_gitlab_http_status(:ok) end end - get(api("/projects/#{project.id}/issues", user)) + it 'skips context fields that do not apply' do + expect(described_class::LOG_FORMATTER).to receive(:call) do |_severity, _datetime, _, data| + expect(data.stringify_keys) + .to include('correlation_id' => an_instance_of(String), + 'meta.caller_id' => 'GET /api/:version/broadcast_messages', + 'meta.remote_ip' => an_instance_of(String), + 'meta.client_id' => a_string_matching(%r{\Aip/.+}), + 'meta.feature_category' => 'navigation', + 'route' => '/api/:version/broadcast_messages') + + expect(data.stringify_keys).not_to include('meta.project', 'meta.root_namespace', 'meta.user') + end + + get(api('/broadcast_messages')) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when there is an unsupported media type' do + it 'logs the route and context metadata for the client' do + expect(described_class::LOG_FORMATTER).to receive(:call) do |_severity, _datetime, _, data| + expect(data.stringify_keys) + .to include('correlation_id' => an_instance_of(String), + 'meta.remote_ip' => an_instance_of(String), + 'meta.client_id' => a_string_matching(%r{\Aip/.+}), + 'route' => '/api/:version/users/:id') + + expect(data.stringify_keys).not_to include('meta.caller_id', 'meta.feature_category', 'meta.user') + end + + put(api("/users/#{user.id}", user), params: { 'name' => 'Test' }, headers: { 'Content-Type' => 'image/png' }) + + expect(response).to have_gitlab_http_status(:unsupported_media_type) + end end - it 'skips fields that do not apply' do - allow_any_instance_of(Gitlab::GrapeLogging::Loggers::ContextLogger).to receive(:parameters) do - Gitlab::ApplicationContext.current.tap do |log_context| - expect(log_context).to match('correlation_id' => an_instance_of(String), - 'meta.caller_id' => 'GET /api/:version/users', - 'meta.remote_ip' => an_instance_of(String), - 'meta.client_id' => an_instance_of(String), - 'meta.feature_category' => 'users') + context 'when there is an OPTIONS request' do + it 'logs the route and context metadata for the client' do + expect(described_class::LOG_FORMATTER).to receive(:call) do |_severity, _datetime, _, data| + expect(data.stringify_keys) + .to include('correlation_id' => an_instance_of(String), + 'meta.remote_ip' => an_instance_of(String), + 'meta.client_id' => a_string_matching(%r{\Auser/.+}), + 'meta.user' => user.username, + 'meta.feature_category' => 'users', + 'route' => '/api/:version/users') + + expect(data.stringify_keys).not_to include('meta.caller_id') end + + options(api('/users', user)) + + expect(response).to have_gitlab_http_status(:no_content) end + end - get(api('/users')) + context 'when the API version is not matched' do + it 'logs the route and context metadata for the client' do + expect(described_class::LOG_FORMATTER).to receive(:call) do |_severity, _datetime, _, data| + expect(data.stringify_keys) + .to include('correlation_id' => an_instance_of(String), + 'meta.remote_ip' => an_instance_of(String), + 'meta.client_id' => a_string_matching(%r{\Aip/.+}), + 'route' => '/api/:version/*path') + + expect(data.stringify_keys).not_to include('meta.caller_id', 'meta.user') + end + + get('/api/v4_or_is_it') + + expect(response).to have_gitlab_http_status(:not_found) + end end end diff --git a/spec/requests/api/bulk_imports_spec.rb b/spec/requests/api/bulk_imports_spec.rb index 1a28687c830..1602819a02e 100644 --- a/spec/requests/api/bulk_imports_spec.rb +++ b/spec/requests/api/bulk_imports_spec.rb @@ -21,6 +21,15 @@ RSpec.describe API::BulkImports do end describe 'POST /bulk_imports' do + before do + allow_next_instance_of(BulkImports::Clients::HTTP) do |instance| + allow(instance) + .to receive(:instance_version) + .and_return( + Gitlab::VersionInfo.new(::BulkImport::MIN_MAJOR_VERSION, ::BulkImport::MIN_MINOR_VERSION_FOR_PROJECT)) + end + end + it 'starts a new migration' do post api('/bulk_imports', user), params: { configuration: { diff --git a/spec/requests/api/ci/resource_groups_spec.rb b/spec/requests/api/ci/resource_groups_spec.rb new file mode 100644 index 00000000000..f5b68557a0d --- /dev/null +++ b/spec/requests/api/ci/resource_groups_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ci::ResourceGroups do + let_it_be(:project) { create(:project) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } } + + let(:user) { developer } + + describe 'GET /projects/:id/resource_groups/:key' do + subject { get api("/projects/#{project.id}/resource_groups/#{key}", user) } + + let!(:resource_group) { create(:ci_resource_group, project: project) } + let(:key) { resource_group.key } + + it 'returns a resource group', :aggregate_failures do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['id']).to eq(resource_group.id) + expect(json_response['key']).to eq(resource_group.key) + expect(json_response['process_mode']).to eq(resource_group.process_mode) + expect(Time.parse(json_response['created_at'])).to be_like_time(resource_group.created_at) + expect(Time.parse(json_response['updated_at'])).to be_like_time(resource_group.updated_at) + end + + context 'when user is reporter' do + let(:user) { reporter } + + it 'returns forbidden' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when there is no corresponding resource group' do + let(:key) { 'unknown' } + + it 'returns not found' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + describe 'PUT /projects/:id/resource_groups/:key' do + subject { put api("/projects/#{project.id}/resource_groups/#{key}", user), params: params } + + let!(:resource_group) { create(:ci_resource_group, project: project) } + let(:key) { resource_group.key } + let(:params) { { process_mode: :oldest_first } } + + it 'changes the process mode of a resource group' do + expect { subject } + .to change { resource_group.reload.process_mode }.from('unordered').to('oldest_first') + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['process_mode']).to eq('oldest_first') + end + + context 'with invalid parameter' do + let(:params) { { process_mode: :unknown } } + + it 'returns bad request' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'when user is reporter' do + let(:user) { reporter } + + it 'returns forbidden' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when there is no corresponding resource group' do + let(:key) { 'unknown' } + + it 'returns not found' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end +end diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb index adac81ff6f4..c3fbef9be48 100644 --- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb +++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb @@ -816,7 +816,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do subject { request_job(id: job.id) } - it_behaves_like 'storing arguments in the application context' do + it_behaves_like 'storing arguments in the application context for the API' do let(:expected_params) { { user: user.username, project: project.full_path, client_id: "user/#{user.id}" } } end @@ -827,7 +827,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end context 'when the runner is of project type' do - it_behaves_like 'storing arguments in the application context' do + it_behaves_like 'storing arguments in the application context for the API' do let(:expected_params) { { project: project.full_path, client_id: "runner/#{runner.id}" } } end @@ -841,7 +841,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do let(:group) { create(:group) } let(:runner) { create(:ci_runner, :group, groups: [group]) } - it_behaves_like 'storing arguments in the application context' do + it_behaves_like 'storing arguments in the application context for the API' do let(:expected_params) { { root_namespace: group.full_path_components.first, client_id: "runner/#{runner.id}" } } end diff --git a/spec/requests/api/ci/runner/runners_delete_spec.rb b/spec/requests/api/ci/runner/runners_delete_spec.rb index 6c6c465f161..9d1bae7cce8 100644 --- a/spec/requests/api/ci/runner/runners_delete_spec.rb +++ b/spec/requests/api/ci/runner/runners_delete_spec.rb @@ -51,7 +51,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do let(:params) { { token: runner.token } } end - it_behaves_like 'storing arguments in the application context' do + it_behaves_like 'storing arguments in the application context for the API' do let(:expected_params) { { client_id: "runner/#{runner.id}" } } end end diff --git a/spec/requests/api/ci/runner/runners_post_spec.rb b/spec/requests/api/ci/runner/runners_post_spec.rb index 17b988a60c5..b3a7d591c93 100644 --- a/spec/requests/api/ci/runner/runners_post_spec.rb +++ b/spec/requests/api/ci/runner/runners_post_spec.rb @@ -58,7 +58,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do expect(runner).to be_instance_type end - it_behaves_like 'storing arguments in the application context' do + it_behaves_like 'storing arguments in the application context for the API' do subject { request } let(:expected_params) { { client_id: "runner/#{::Ci::Runner.first.id}" } } @@ -84,7 +84,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do expect(runner).to be_project_type end - it_behaves_like 'storing arguments in the application context' do + it_behaves_like 'storing arguments in the application context for the API' do subject { request } let(:expected_params) { { project: project.full_path, client_id: "runner/#{::Ci::Runner.first.id}" } } @@ -190,7 +190,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do expect(runner).to be_group_type end - it_behaves_like 'storing arguments in the application context' do + it_behaves_like 'storing arguments in the application context for the API' do subject { request } let(:expected_params) { { root_namespace: group.full_path_components.first, client_id: "runner/#{::Ci::Runner.first.id}" } } diff --git a/spec/requests/api/ci/runner/runners_verify_post_spec.rb b/spec/requests/api/ci/runner/runners_verify_post_spec.rb index c2e97446738..4680076acae 100644 --- a/spec/requests/api/ci/runner/runners_verify_post_spec.rb +++ b/spec/requests/api/ci/runner/runners_verify_post_spec.rb @@ -45,7 +45,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do expect(response).to have_gitlab_http_status(:ok) end - it_behaves_like 'storing arguments in the application context' do + it_behaves_like 'storing arguments in the application context for the API' do let(:expected_params) { { client_id: "runner/#{runner.id}" } } end end diff --git a/spec/requests/api/ci/runners_reset_registration_token_spec.rb b/spec/requests/api/ci/runners_reset_registration_token_spec.rb index 7623d3f1b17..df64c0bd22b 100644 --- a/spec/requests/api/ci/runners_reset_registration_token_spec.rb +++ b/spec/requests/api/ci/runners_reset_registration_token_spec.rb @@ -118,7 +118,7 @@ RSpec.describe API::Ci::Runners do end include_context 'when authorized', 'group' do - let_it_be(:user) { create_default(:group_member, :maintainer, user: create(:user), group: group ).user } + let_it_be(:user) { create_default(:group_member, :owner, user: create(:user), group: group ).user } def get_token group.reload.runners_token diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb index 902938d7d02..6879dfc9572 100644 --- a/spec/requests/api/ci/runners_spec.rb +++ b/spec/requests/api/ci/runners_spec.rb @@ -291,6 +291,16 @@ RSpec.describe API::Ci::Runners do end end + context 'when the runner is a group runner' do + it "returns the runner's details" do + get api("/runners/#{group_runner_a.id}", admin) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['description']).to eq(group_runner_a.description) + expect(json_response['groups'].first['id']).to eq(group.id) + end + end + context "runner project's administrative user" do context 'when runner is not shared' do it "returns runner's details" do @@ -600,6 +610,94 @@ RSpec.describe API::Ci::Runners do end end + describe 'POST /runners/:id/reset_authentication_token' do + context 'admin user' do + it 'resets shared runner authentication token' do + expect do + post api("/runners/#{shared_runner.id}/reset_authentication_token", admin) + + expect(response).to have_gitlab_http_status(:success) + expect(json_response).to eq({ 'token' => shared_runner.reload.token }) + end.to change { shared_runner.reload.token } + end + + it 'returns 404 if runner does not exist' do + post api('/runners/0/reset_authentication_token', admin) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'authorized user' do + it 'does not reset project runner authentication token without access to it' do + expect do + post api("/runners/#{project_runner.id}/reset_authentication_token", user2) + expect(response).to have_gitlab_http_status(:forbidden) + end.not_to change { project_runner.reload.token } + end + + it 'resets project runner authentication token for owned project' do + expect do + post api("/runners/#{project_runner.id}/reset_authentication_token", user) + + expect(response).to have_gitlab_http_status(:success) + expect(json_response).to eq({ 'token' => project_runner.reload.token }) + end.to change { project_runner.reload.token } + end + + it 'does not reset group runner authentication token with guest access' do + expect do + post api("/runners/#{group_runner_a.id}/reset_authentication_token", group_guest) + + expect(response).to have_gitlab_http_status(:forbidden) + end.not_to change { group_runner_a.reload.token } + end + + it 'does not reset group runner authentication token with reporter access' do + expect do + post api("/runners/#{group_runner_a.id}/reset_authentication_token", group_reporter) + + expect(response).to have_gitlab_http_status(:forbidden) + end.not_to change { group_runner_a.reload.token } + end + + it 'does not reset group runner authentication token with developer access' do + expect do + post api("/runners/#{group_runner_a.id}/reset_authentication_token", group_developer) + + expect(response).to have_gitlab_http_status(:forbidden) + end.not_to change { group_runner_a.reload.token } + end + + it 'does not reset group runner authentication token with maintainer access' do + expect do + post api("/runners/#{group_runner_a.id}/reset_authentication_token", group_maintainer) + + expect(response).to have_gitlab_http_status(:forbidden) + end.not_to change { group_runner_a.reload.token } + end + + it 'resets group runner authentication token with owner access' do + expect do + post api("/runners/#{group_runner_a.id}/reset_authentication_token", user) + + expect(response).to have_gitlab_http_status(:success) + expect(json_response).to eq({ 'token' => group_runner_a.reload.token }) + end.to change { group_runner_a.reload.token } + end + end + + context 'unauthorized user' do + it 'does not reset authentication token' do + expect do + post api("/runners/#{shared_runner.id}/reset_authentication_token") + + expect(response).to have_gitlab_http_status(:unauthorized) + end.not_to change { shared_runner.reload.token } + end + end + end + describe 'GET /runners/:id/jobs' do let_it_be(:job_1) { create(:ci_build) } let_it_be(:job_2) { create(:ci_build, :running, runner: shared_runner, project: project) } diff --git a/spec/requests/api/ci/triggers_spec.rb b/spec/requests/api/ci/triggers_spec.rb index 410e2ae405e..d270a16d28d 100644 --- a/spec/requests/api/ci/triggers_spec.rb +++ b/spec/requests/api/ci/triggers_spec.rb @@ -131,7 +131,7 @@ RSpec.describe API::Ci::Triggers do let(:subject_proc) { proc { post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), params: { ref: 'refs/heads/other-branch' } } } context 'when triggering a pipeline from a trigger token' do - it_behaves_like 'storing arguments in the application context' + it_behaves_like 'storing arguments in the application context for the API' it_behaves_like 'not executing any extra queries for the application context' end @@ -142,7 +142,7 @@ RSpec.describe API::Ci::Triggers do context 'when other job is triggered by a user' do let(:trigger_token) { create(:ci_build, :running, project: project, user: user).token } - it_behaves_like 'storing arguments in the application context' + it_behaves_like 'storing arguments in the application context for the API' it_behaves_like 'not executing any extra queries for the application context' end @@ -151,7 +151,7 @@ RSpec.describe API::Ci::Triggers do let(:runner) { create(:ci_runner) } let(:expected_params) { { client_id: "runner/#{runner.id}", project: project.full_path } } - it_behaves_like 'storing arguments in the application context' + it_behaves_like 'storing arguments in the application context for the API' it_behaves_like 'not executing any extra queries for the application context', 1 end end diff --git a/spec/requests/api/container_repositories_spec.rb b/spec/requests/api/container_repositories_spec.rb index 8d7494ffce1..9809702467d 100644 --- a/spec/requests/api/container_repositories_spec.rb +++ b/spec/requests/api/container_repositories_spec.rb @@ -48,6 +48,19 @@ RSpec.describe API::ContainerRepositories do expect(response).to match_response_schema('registry/repository') end + context 'with a network error' do + before do + stub_container_registry_network_error(client_method: :repository_tags) + end + + it 'returns a matching schema' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('registry/repository') + end + end + context 'with tags param' do let(:url) { "/registry/repositories/#{repository.id}?tags=true" } @@ -61,6 +74,19 @@ RSpec.describe API::ContainerRepositories do expect(json_response['id']).to eq(repository.id) expect(response.body).to include('tags') end + + context 'with a network error' do + before do + stub_container_registry_network_error(client_method: :repository_tags) + end + + it 'returns a connection error message' do + subject + + expect(response).to have_gitlab_http_status(:service_unavailable) + expect(json_response['message']).to include('We are having trouble connecting to the Container Registry') + end + end end context 'with tags_count param' do diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index 38c96cd37af..69f7b54c277 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -376,6 +376,16 @@ RSpec.describe API::Deployments do expect(json_response['status']).to eq('success') end + it 'returns an error when an invalid status transition is detected' do + put( + api("/projects/#{project.id}/deployments/#{deploy.id}", user), + params: { status: 'running' } + ) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']['status']).to include(%Q{cannot transition via \"run\"}) + end + it 'links merge requests when the deployment status changes to success', :sidekiq_inline do mr = create( :merge_request, diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index bc7bb7523c9..5fb24dc91a4 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -18,6 +18,7 @@ RSpec.describe API::Environments do get api("/projects/#{project.id}/environments", user) expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/environments') expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(1) @@ -167,6 +168,7 @@ RSpec.describe API::Environments do post api("/projects/#{project.id}/environments", user), params: { name: "mepmep" } expect(response).to have_gitlab_http_status(:created) + expect(response).to match_response_schema('public_api/v4/environment') expect(json_response['name']).to eq('mepmep') expect(json_response['slug']).to eq('mepmep') expect(json_response['external']).to be nil @@ -212,6 +214,7 @@ RSpec.describe API::Environments do params: { name: 'Mepmep', external_url: url } expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/environment') expect(json_response['name']).to eq('Mepmep') expect(json_response['external_url']).to eq(url) end @@ -250,7 +253,7 @@ RSpec.describe API::Environments do expect(response).to have_gitlab_http_status(:forbidden) end - it 'returns a 200 for stopped environment' do + it 'returns a 204 for stopped environment' do environment.stop delete api("/projects/#{project.id}/environments/#{environment.id}", user) @@ -294,6 +297,7 @@ RSpec.describe API::Environments do it 'returns a 200' do expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/environment') end it 'actually stops the environment' do @@ -327,6 +331,7 @@ RSpec.describe API::Environments do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('public_api/v4/environment') + expect(json_response['last_deployment']).to be_present end end diff --git a/spec/requests/api/error_tracking_client_keys_spec.rb b/spec/requests/api/error_tracking/client_keys_spec.rb index 886ec5ade3d..00c1e8799e6 100644 --- a/spec/requests/api/error_tracking_client_keys_spec.rb +++ b/spec/requests/api/error_tracking/client_keys_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::ErrorTrackingClientKeys do +RSpec.describe API::ErrorTracking::ClientKeys do let_it_be(:guest) { create(:user) } let_it_be(:maintainer) { create(:user) } let_it_be(:setting) { create(:project_error_tracking_setting) } diff --git a/spec/requests/api/error_tracking_collector_spec.rb b/spec/requests/api/error_tracking/collector_spec.rb index 35d3ea01f87..7acadeb1287 100644 --- a/spec/requests/api/error_tracking_collector_spec.rb +++ b/spec/requests/api/error_tracking/collector_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::ErrorTrackingCollector do +RSpec.describe API::ErrorTracking::Collector do let_it_be(:project) { create(:project, :private) } let_it_be(:setting) { create(:project_error_tracking_setting, :integrated, project: project) } let_it_be(:client_key) { create(:error_tracking_client_key, project: project) } diff --git a/spec/requests/api/error_tracking_spec.rb b/spec/requests/api/error_tracking/project_settings_spec.rb index ec9a3378acc..161e4f01ea5 100644 --- a/spec/requests/api/error_tracking_spec.rb +++ b/spec/requests/api/error_tracking/project_settings_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::ErrorTracking do +RSpec.describe API::ErrorTracking::ProjectSettings do let_it_be(:user) { create(:user) } let(:setting) { create(:project_error_tracking_setting) } diff --git a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb index 008241b8055..241c658441b 100644 --- a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb @@ -30,7 +30,7 @@ RSpec.describe 'get board lists' do nodes { lists { nodes { - issues(filters: {labelName: "#{label2.title}"}) { + issues(filters: {labelName: "#{label2.title}"}, first: 3) { count nodes { #{all_graphql_fields_for('issues'.classify)} @@ -44,6 +44,10 @@ RSpec.describe 'get board lists' do ) end + def issue_id + issues_data.map { |i| i['id'] } + end + def issue_titles issues_data.map { |i| i['title'] } end @@ -60,6 +64,7 @@ RSpec.describe 'get board lists' do let!(:issue3) { create(:issue, project: issue_project, labels: [label, label2], relative_position: nil) } let!(:issue4) { create(:issue, project: issue_project, labels: [label], relative_position: 9) } let!(:issue5) { create(:issue, project: issue_project, labels: [label2], relative_position: 432) } + let!(:issue6) { create(:issue, project: issue_project, labels: [label, label2], relative_position: nil) } context 'when the user does not have access to the board' do it 'returns nil' do @@ -72,14 +77,19 @@ RSpec.describe 'get board lists' do context 'when user can read the board' do before do board_parent.add_reporter(user) + post_graphql(query("id: \"#{global_id_of(label_list)}\""), current_user: user) end it 'can access the issues', :aggregate_failures do - post_graphql(query("id: \"#{global_id_of(label_list)}\""), current_user: user) - + # ties for relative positions are broken by id in ascending order by default expect(issue_titles).to eq([issue2.title, issue1.title, issue3.title]) expect(issue_relative_positions).not_to include(nil) end + + it 'does not set the relative positions of the issues not being returned', :aggregate_failures do + expect(issue_id).not_to include(issue6.id) + expect(issue3.relative_position).to be_nil + end end end diff --git a/spec/requests/api/graphql/boards/board_list_query_spec.rb b/spec/requests/api/graphql/boards/board_list_query_spec.rb new file mode 100644 index 00000000000..dec7ca715f2 --- /dev/null +++ b/spec/requests/api/graphql/boards/board_list_query_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Querying a Board list' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:board) { create(:board, resource_parent: project) } + let_it_be(:label) { create(:label, project: project, name: 'foo') } + let_it_be(:list) { create(:list, board: board, label: label) } + let_it_be(:issue1) { create(:issue, project: project, labels: [label]) } + let_it_be(:issue2) { create(:issue, project: project, labels: [label], assignees: [current_user]) } + + let(:filters) { {} } + let(:query) do + graphql_query_for( + :board_list, + { id: list.to_global_id.to_s, issueFilters: filters }, + %w[title issuesCount] + ) + end + + subject { graphql_data['boardList'] } + + before do + post_graphql(query, current_user: current_user) + end + + context 'when the user has access to the list' do + before_all do + project.add_guest(current_user) + end + + it_behaves_like 'a working graphql query' + + it { is_expected.to include({ 'issuesCount' => 2, 'title' => list.title }) } + + context 'with matching issue filters' do + let(:filters) { { assigneeUsername: current_user.username } } + + it 'filters issues metadata' do + is_expected.to include({ 'issuesCount' => 1, 'title' => list.title }) + end + end + + context 'with unmatching issue filters' do + let(:filters) { { assigneeUsername: 'foo' } } + + it 'filters issues metadata' do + is_expected.to include({ 'issuesCount' => 0, 'title' => list.title }) + end + end + end + + context 'when the user does not have access to the list' do + it { is_expected.to be_nil } + end + + context 'when ID argument is missing' do + let(:query) do + graphql_query_for('boardList', {}, 'title') + end + + it 'raises an exception' do + expect(graphql_errors).to include(a_hash_including('message' => "Field 'boardList' is missing required arguments: id")) + end + end + + context 'when list ID is not found' do + let(:query) do + graphql_query_for('boardList', { id: "gid://gitlab/List/#{non_existing_record_id}" }, 'title') + end + + it { is_expected.to be_nil } + end + + it 'does not have an N+1 performance issue' do + a, b = create_list(:list, 2, board: board) + ctx = { current_user: current_user } + project.add_guest(current_user) + + baseline = graphql_query_for(:board_list, { id: global_id_of(a) }, 'title') + query = <<~GQL + query { + a: #{query_graphql_field(:board_list, { id: global_id_of(a) }, 'title')} + b: #{query_graphql_field(:board_list, { id: global_id_of(b) }, 'title')} + } + GQL + + control = ActiveRecord::QueryRecorder.new do + run_with_clean_state(baseline, context: ctx) + end + + expect { run_with_clean_state(query, context: ctx) }.not_to exceed_query_limit(control) + end +end diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb index 2d52cddcacc..ace8c59e82d 100644 --- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb @@ -92,9 +92,9 @@ RSpec.describe 'get board lists' do context 'when ascending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { } - let(:first_param) { 2 } - let(:expected_results) { lists.map { |list| global_id_of(list) } } + let(:sort_param) { } + let(:first_param) { 2 } + let(:all_records) { lists.map { |list| global_id_of(list) } } end end end diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index 74547196445..ab53ff654e9 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -6,6 +6,7 @@ RSpec.describe 'Query.runner(id)' do include GraphqlHelpers let_it_be(:user) { create(:user, :admin) } + let_it_be(:group) { create(:group) } let_it_be(:active_instance_runner) do create(:ci_runner, :instance, description: 'Runner 1', contacted_at: 2.hours.ago, @@ -18,12 +19,20 @@ RSpec.describe 'Query.runner(id)' do version: 'adfe157', revision: 'b', ip_address: '10.10.10.10', access_level: 1, run_untagged: true) end + let_it_be(:active_group_runner) do + create(:ci_runner, :group, groups: [group], description: 'Group runner 1', contacted_at: 2.hours.ago, + active: true, version: 'adfe156', revision: 'a', locked: true, ip_address: '127.0.0.1', maximum_timeout: 600, + access_level: 0, tag_list: %w[tag1 tag2], run_untagged: true) + end + def get_runner(id) case id when :active_instance_runner active_instance_runner when :inactive_instance_runner inactive_instance_runner + when :active_group_runner + active_group_runner end end @@ -61,7 +70,39 @@ RSpec.describe 'Query.runner(id)' do 'ipAddress' => runner.ip_address, 'runnerType' => runner.instance_type? ? 'INSTANCE_TYPE' : 'PROJECT_TYPE', 'jobCount' => 0, - 'projectCount' => nil + 'projectCount' => nil, + 'adminUrl' => "http://localhost/admin/runners/#{runner.id}", + 'userPermissions' => { + 'readRunner' => true, + 'updateRunner' => true, + 'deleteRunner' => true + } + ) + expect(runner_data['tagList']).to match_array runner.tag_list + end + end + + shared_examples 'retrieval with no admin url' do |runner_id| + let(:query) do + wrap_fields(query_graphql_path(query_path, all_graphql_fields_for('CiRunner'))) + end + + let(:query_path) do + [ + [:runner, { id: get_runner(runner_id).to_global_id.to_s }] + ] + end + + it 'retrieves expected fields' do + post_graphql(query, current_user: user) + + runner_data = graphql_data_at(:runner) + expect(runner_data).not_to be_nil + + runner = get_runner(runner_id) + expect(runner_data).to match a_hash_including( + 'id' => "gid://gitlab/Ci::Runner/#{runner.id}", + 'adminUrl' => nil ) expect(runner_data['tagList']).to match_array runner.tag_list end @@ -147,6 +188,39 @@ RSpec.describe 'Query.runner(id)' do it_behaves_like 'runner details fetch', :inactive_instance_runner end + describe 'for runner inside group request' do + let(:query) do + %( + query { + group(fullPath: "#{group.full_path}") { + runners { + edges { + webUrl + node { + id + } + } + } + } + } + ) + end + + it 'retrieves webUrl field with expected value' do + post_graphql(query, current_user: user) + + runner_data = graphql_data_at(:group, :runners, :edges) + expect(runner_data).to match_array [ + a_hash_including( + 'webUrl' => "http://localhost/groups/#{group.full_path}/-/runners/#{active_group_runner.id}", + 'node' => { + 'id' => "gid://gitlab/Ci::Runner/#{active_group_runner.id}" + } + ) + ] + end + end + describe 'for multiple runners' do let_it_be(:project1) { create(:project, :test_repo) } let_it_be(:project2) { create(:project, :test_repo) } @@ -176,7 +250,7 @@ RSpec.describe 'Query.runner(id)' do end before do - project_runner2.projects.clear + project_runner2.runner_projects.clear post_graphql(query, current_user: user) end @@ -205,6 +279,16 @@ RSpec.describe 'Query.runner(id)' do it_behaves_like 'retrieval by unauthorized user', :active_instance_runner end + describe 'by non-admin user' do + let(:user) { create(:user) } + + before do + group.add_user(user, Gitlab::Access::OWNER) + end + + it_behaves_like 'retrieval with no admin url', :active_group_runner + end + describe 'by unauthenticated user' do let(:user) { nil } diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb index 778fe5b129e..51a07e60e15 100644 --- a/spec/requests/api/graphql/ci/runners_spec.rb +++ b/spec/requests/api/graphql/ci/runners_spec.rb @@ -95,9 +95,9 @@ RSpec.describe 'Query.runners' do let(:ordered_runners) { runners.sort_by(&:contacted_at) } it_behaves_like 'sorted paginated query' do - let(:sort_param) { :CONTACTED_ASC } - let(:first_param) { 2 } - let(:expected_results) { ordered_runners.map(&:id) } + let(:sort_param) { :CONTACTED_ASC } + let(:first_param) { 2 } + let(:all_records) { ordered_runners.map(&:id) } end end @@ -105,9 +105,9 @@ RSpec.describe 'Query.runners' do let(:ordered_runners) { runners.sort_by(&:created_at).reverse } it_behaves_like 'sorted paginated query' do - let(:sort_param) { :CREATED_DESC } - let(:first_param) { 2 } - let(:expected_results) { ordered_runners.map(&:id) } + let(:sort_param) { :CREATED_DESC } + let(:first_param) { 2 } + let(:all_records) { ordered_runners.map(&:id) } end end end diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb index 356e1e11def..d93afcc0f33 100644 --- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb +++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb @@ -153,4 +153,6 @@ RSpec.describe 'container repository details' do end end end + + it_behaves_like 'handling graphql network errors with the container registry' end diff --git a/spec/requests/api/graphql/group/container_repositories_spec.rb b/spec/requests/api/graphql/group/container_repositories_spec.rb index 939d7791d92..be0b866af4a 100644 --- a/spec/requests/api/graphql/group/container_repositories_spec.rb +++ b/spec/requests/api/graphql/group/container_repositories_spec.rb @@ -14,11 +14,12 @@ RSpec.describe 'getting container repositories in a group' do let_it_be(:container_repositories) { [container_repository, container_repositories_delete_scheduled, container_repositories_delete_failed].flatten } let_it_be(:container_expiration_policy) { project.container_expiration_policy } + let(:excluded_fields) { [] } let(:container_repositories_fields) do <<~GQL edges { node { - #{all_graphql_fields_for('container_repositories'.classify, max_depth: 1)} + #{all_graphql_fields_for('container_repositories'.classify, max_depth: 1, excluded: excluded_fields)} } } GQL @@ -152,6 +153,12 @@ RSpec.describe 'getting container repositories in a group' do end end + it_behaves_like 'handling graphql network errors with the container registry' + + it_behaves_like 'not hitting graphql network errors with the container registry' do + let(:excluded_fields) { %w[tags tagsCount] } + end + it 'returns the total count of container repositories' do subject diff --git a/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb index c5c6d85d1e6..de3dbc5c324 100644 --- a/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb +++ b/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb @@ -33,46 +33,59 @@ RSpec.describe 'getting dependency proxy settings for a group' do before do stub_config(dependency_proxy: { enabled: true }) - group.create_dependency_proxy_setting!(enabled: true) end subject { post_graphql(query, current_user: user, variables: variables) } - it_behaves_like 'a working graphql query' do - before do - subject - end - end - - context 'with different permissions' do - where(:group_visibility, :role, :access_granted) do - :private | :maintainer | true - :private | :developer | true - :private | :reporter | true - :private | :guest | true - :private | :anonymous | false - :public | :maintainer | true - :public | :developer | true - :public | :reporter | true - :public | :guest | true - :public | :anonymous | false + shared_examples 'dependency proxy group setting query' do + it_behaves_like 'a working graphql query' do + before do + subject + end end - with_them do - before do - group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false)) - group.add_user(user, role) unless role == :anonymous + context 'with different permissions' do + where(:group_visibility, :role, :access_granted) do + :private | :maintainer | true + :private | :developer | true + :private | :reporter | true + :private | :guest | true + :private | :anonymous | false + :public | :maintainer | true + :public | :developer | true + :public | :reporter | true + :public | :guest | true + :public | :anonymous | false end - it 'return the proper response' do - subject + with_them do + before do + group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false)) + group.add_user(user, role) unless role == :anonymous + end + + it 'return the proper response' do + subject - if access_granted - expect(dependency_proxy_group_setting_response).to eq('enabled' => true) - else - expect(dependency_proxy_group_setting_response).to be_blank + if access_granted + expect(dependency_proxy_group_setting_response).to eq('enabled' => true) + else + expect(dependency_proxy_group_setting_response).to be_blank + end end end end end + + context 'with the settings model created' do + before do + group.create_dependency_proxy_setting!(enabled: true) + end + + it_behaves_like 'dependency proxy group setting query' + end + + context 'without the settings model created' do + it_behaves_like 'dependency proxy group setting query' + end end diff --git a/spec/requests/api/graphql/group/issues_spec.rb b/spec/requests/api/graphql/group/issues_spec.rb new file mode 100644 index 00000000000..332bf242e9c --- /dev/null +++ b/spec/requests/api/graphql/group/issues_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting an issue list for a group' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:group1) { create(:group) } + let_it_be(:group2) { create(:group) } + let_it_be(:project1) { create(:project, :public, group: group1) } + let_it_be(:project2) { create(:project, :private, group: group1) } + let_it_be(:project3) { create(:project, :public, group: group2) } + let_it_be(:issue1) { create(:issue, project: project1) } + let_it_be(:issue2) { create(:issue, project: project2) } + let_it_be(:issue3) { create(:issue, project: project3) } + + let(:issue1_gid) { issue1.to_global_id.to_s } + let(:issue2_gid) { issue2.to_global_id.to_s } + let(:issues_data) { graphql_data['group']['issues']['edges'] } + let(:issue_filter_params) { {} } + + let(:fields) do + <<~QUERY + edges { + node { + #{all_graphql_fields_for('issues'.classify)} + } + } + QUERY + end + + let(:query) do + graphql_query_for( + 'group', + { 'fullPath' => group1.full_path }, + query_graphql_field('issues', issue_filter_params, fields) + ) + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end + + context 'when there is a confidential issue' do + let_it_be(:confidential_issue1) { create(:issue, :confidential, project: project1) } + let_it_be(:confidential_issue2) { create(:issue, :confidential, project: project2) } + let_it_be(:confidential_issue3) { create(:issue, :confidential, project: project3) } + + let(:confidential_issue1_gid) { confidential_issue1.to_global_id.to_s } + let(:confidential_issue2_gid) { confidential_issue2.to_global_id.to_s } + + context 'when the user cannot see confidential issues' do + before do + group1.add_guest(current_user) + end + + it 'returns issues without confidential issues for the group' do + post_graphql(query, current_user: current_user) + + expect(issues_ids).to contain_exactly(issue1_gid, issue2_gid) + end + + context 'filtering for confidential issues' do + let(:issue_filter_params) { { confidential: true } } + + it 'returns no issues' do + post_graphql(query, current_user: current_user) + + expect(issues_ids).to be_empty + end + end + + context 'filtering for non-confidential issues' do + let(:issue_filter_params) { { confidential: false } } + + it 'returns correctly filtered issues' do + post_graphql(query, current_user: current_user) + + expect(issues_ids).to contain_exactly(issue1_gid, issue2_gid) + end + end + end + + context 'when the user can see confidential issues' do + before do + group1.add_developer(current_user) + end + + it 'returns issues with confidential issues for the group' do + post_graphql(query, current_user: current_user) + + expect(issues_ids).to contain_exactly(issue1_gid, issue2_gid, confidential_issue1_gid, confidential_issue2_gid) + end + + context 'filtering for confidential issues' do + let(:issue_filter_params) { { confidential: true } } + + it 'returns correctly filtered issues' do + post_graphql(query, current_user: current_user) + + expect(issues_ids).to contain_exactly(confidential_issue1_gid, confidential_issue2_gid) + end + end + + context 'filtering for non-confidential issues' do + let(:issue_filter_params) { { confidential: false } } + + it 'returns correctly filtered issues' do + post_graphql(query, current_user: current_user) + + expect(issues_ids).to contain_exactly(issue1_gid, issue2_gid) + end + end + end + end + + def issues_ids + graphql_dig_at(issues_data, :node, :id) + end +end diff --git a/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb index 07b05ead651..0fd8fdc3f59 100644 --- a/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/runners_registration_token/reset_spec.rb @@ -89,7 +89,7 @@ RSpec.describe 'RunnersRegistrationTokenReset' do end include_context 'when authorized', 'group' do - let_it_be(:user) { create_default(:group_member, :maintainer, user: create(:user), group: group ).user } + let_it_be(:user) { create_default(:group_member, :owner, user: create(:user), group: group ).user } def get_token group.reload.runners_token diff --git a/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb b/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb new file mode 100644 index 00000000000..aac8eb22771 --- /dev/null +++ b/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Create a new cluster agent token' do + include GraphqlHelpers + + let_it_be(:cluster_agent) { create(:cluster_agent) } + let_it_be(:current_user) { create(:user) } + + let(:description) { 'create token' } + let(:name) { 'token name' } + let(:mutation) do + graphql_mutation( + :cluster_agent_token_create, + { cluster_agent_id: cluster_agent.to_global_id.to_s, description: description, name: name } + ) + end + + def mutation_response + graphql_mutation_response(:cluster_agent_token_create) + end + + context 'without user permissions' do + it_behaves_like 'a mutation that returns top-level errors', + errors: ["The resource that you are attempting to access does not exist "\ + "or you don't have permission to perform this action"] + + it 'does not create a token' do + expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Clusters::AgentToken, :count) + end + end + + context 'with project permissions' do + before do + cluster_agent.project.add_maintainer(current_user) + end + + it 'creates a new token', :aggregate_failures do + expect { post_graphql_mutation(mutation, current_user: current_user) }.to change { Clusters::AgentToken.count }.by(1) + expect(mutation_response['errors']).to eq([]) + end + + it 'returns token information', :aggregate_failures do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['secret']).not_to be_nil + expect(mutation_response.dig('token', 'description')).to eq(description) + expect(mutation_response.dig('token', 'name')).to eq(name) + end + end +end diff --git a/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb b/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb new file mode 100644 index 00000000000..c2ef2362d66 --- /dev/null +++ b/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Create a new cluster agent' do + include GraphqlHelpers + + let(:project) { create(:project, :public, :repository) } + let(:project_name) { 'agent-test' } + let(:current_user) { create(:user) } + + let(:mutation) do + graphql_mutation( + :create_cluster_agent, + { project_path: project.full_path, name: project_name } + ) + end + + def mutation_response + graphql_mutation_response(:create_cluster_agent) + end + + context 'without project permissions' do + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not create cluster agent' do + expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Clusters::Agent, :count) + end + end + + context 'with user permissions' do + before do + project.add_maintainer(current_user) + end + + it 'creates a new cluster agent', :aggregate_failures do + expect { post_graphql_mutation(mutation, current_user: current_user) }.to change { Clusters::Agent.count }.by(1) + expect(mutation_response.dig('clusterAgent', 'name')).to eq(project_name) + expect(mutation_response['errors']).to eq([]) + end + end +end diff --git a/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb b/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb new file mode 100644 index 00000000000..5f6822223ca --- /dev/null +++ b/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Delete a cluster agent' do + include GraphqlHelpers + + let(:cluster_agent) { create(:cluster_agent) } + let(:project) { cluster_agent.project } + let(:current_user) { create(:user) } + + let(:mutation) do + graphql_mutation( + :cluster_agent_delete, + { id: cluster_agent.to_global_id.uri } + ) + end + + def mutation_response + graphql_mutation_response(:cluster_agent_delete) + end + + context 'without project permissions' do + it_behaves_like 'a mutation that returns top-level errors', + errors: ['The resource that you are attempting to access does not exist '\ + 'or you don\'t have permission to perform this action'] + + it 'does not delete cluster agent' do + expect { cluster_agent.reload }.not_to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'with project permissions' do + before do + project.add_maintainer(current_user) + end + + it 'deletes a cluster agent', :aggregate_failures do + expect { post_graphql_mutation(mutation, current_user: current_user) }.to change { Clusters::Agent.count }.by(-1) + expect(mutation_response['errors']).to eq([]) + end + end +end diff --git a/spec/requests/api/graphql/mutations/dependency_proxy/group_settings/update_spec.rb b/spec/requests/api/graphql/mutations/dependency_proxy/group_settings/update_spec.rb new file mode 100644 index 00000000000..f05bf23ad27 --- /dev/null +++ b/spec/requests/api/graphql/mutations/dependency_proxy/group_settings/update_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Updating the dependency proxy group settings' do + include GraphqlHelpers + using RSpec::Parameterized::TableSyntax + + let_it_be(:user) { create(:user) } + + let(:params) do + { + group_path: group.full_path, + enabled: false + } + end + + let(:mutation) do + graphql_mutation(:update_dependency_proxy_settings, params) do + <<~QL + dependencyProxySetting { + enabled + } + errors + QL + end + end + + let(:mutation_response) { graphql_mutation_response(:update_dependency_proxy_settings) } + let(:group_settings) { mutation_response['dependencyProxySetting'] } + + before do + stub_config(dependency_proxy: { enabled: true }) + end + + describe 'post graphql mutation' do + subject { post_graphql_mutation(mutation, current_user: user) } + + let_it_be_with_reload(:group) { create(:group) } + let_it_be_with_reload(:group_settings) { create(:dependency_proxy_group_setting, group: group) } + + context 'without permission' do + it 'returns no response' do + subject + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response).to be_nil + end + end + + context 'with permission' do + before do + group.add_developer(user) + end + + it 'returns the updated dependency proxy settings', :aggregate_failures do + subject + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to be_empty + expect(group_settings[:enabled]).to eq(false) + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb index dec9afd1310..608b36e4f15 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb @@ -115,7 +115,7 @@ RSpec.describe 'Setting assignees of a merge request', :assume_throttled do context 'when passing append as true' do let(:mode) { Types::MutationOperationModeEnum.enum[:append] } let(:input) { { assignee_usernames: [assignee2.username], operation_mode: mode } } - let(:db_query_limit) { 21 } + let(:db_query_limit) { 22 } before do # In CE, APPEND is a NOOP as you can't have multiple assignees diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb index 414847c9c93..d5410f1a7cb 100644 --- a/spec/requests/api/graphql/namespace/projects_spec.rb +++ b/spec/requests/api/graphql/namespace/projects_spec.rb @@ -106,10 +106,10 @@ RSpec.describe 'getting projects' do context 'when sorting by similarity' do it_behaves_like 'sorted paginated query' do - let(:node_path) { %w[name] } - let(:sort_param) { :SIMILARITY } - let(:first_param) { 2 } - let(:expected_results) { [project_3.name, project_2.name, project_4.name] } + let(:node_path) { %w[name] } + let(:sort_param) { :SIMILARITY } + let(:first_param) { 2 } + let(:all_records) { [project_3.name, project_2.name, project_4.name] } end end end diff --git a/spec/requests/api/graphql/project/cluster_agents_spec.rb b/spec/requests/api/graphql/project/cluster_agents_spec.rb new file mode 100644 index 00000000000..dc7254dd552 --- /dev/null +++ b/spec/requests/api/graphql/project/cluster_agents_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Project.cluster_agents' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :public) } + let_it_be(:current_user) { create(:user, maintainer_projects: [project]) } + let_it_be(:agents) { create_list(:cluster_agent, 5, project: project) } + + let(:first) { var('Int') } + let(:cluster_agents_fields) { nil } + let(:project_fields) do + query_nodes(:cluster_agents, cluster_agents_fields, args: { first: first }, max_depth: 3) + end + + let(:query) do + args = { full_path: project.full_path } + + with_signature([first], graphql_query_for(:project, args, project_fields)) + end + + before do + allow(Gitlab::Kas::Client).to receive(:new).and_return(double(get_connected_agents: [])) + end + + it 'can retrieve cluster agents' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(:project, :cluster_agents, :nodes)).to match_array( + agents.map { |agent| a_hash_including('id' => global_id_of(agent)) } + ) + end + + context 'selecting page info' do + let(:project_fields) do + query_nodes(:cluster_agents, args: { first: first }, include_pagination_info: true) + end + + it 'can paginate cluster agents' do + post_graphql(query, current_user: current_user, variables: first.with(2)) + + expect(graphql_data_at(:project, :cluster_agents, :page_info)).to include( + 'hasNextPage' => be_truthy, + 'hasPreviousPage' => be_falsey + ) + expect(graphql_data_at(:project, :cluster_agents, :nodes)).to have_attributes(size: 2) + end + end + + context 'selecting tokens' do + let_it_be(:token_1) { create(:cluster_agent_token, agent: agents.second) } + let_it_be(:token_2) { create(:cluster_agent_token, agent: agents.second, last_used_at: 3.days.ago) } + let_it_be(:token_3) { create(:cluster_agent_token, agent: agents.second, last_used_at: 2.days.ago) } + + let(:cluster_agents_fields) { [:id, query_nodes(:tokens, of: 'ClusterAgentToken')] } + + it 'can select tokens in last_used_at order' do + post_graphql(query, current_user: current_user) + + tokens = graphql_data_at(:project, :cluster_agents, :nodes, :tokens, :nodes) + + expect(tokens).to match([ + a_hash_including('id' => global_id_of(token_3)), + a_hash_including('id' => global_id_of(token_2)), + a_hash_including('id' => global_id_of(token_1)) + ]) + end + + it 'does not suffer from N+1 performance issues' do + post_graphql(query, current_user: current_user) + + expect do + post_graphql(query, current_user: current_user) + end.to issue_same_number_of_queries_as { post_graphql(query, current_user: current_user, variables: [first.with(1)]) } + end + end + + context 'selecting connections' do + let(:agent_meta) { double(version: '1', commit_id: 'abc', pod_namespace: 'namespace', pod_name: 'pod') } + let(:connected_agent) { double(agent_id: agents.first.id, connected_at: 123456, connection_id: 1, agent_meta: agent_meta) } + + let(:metadata_fields) { query_graphql_field(:metadata, {}, [:version, :commit, :pod_namespace, :pod_name], 'AgentMetadata') } + let(:cluster_agents_fields) { [:id, query_nodes(:connections, [:connection_id, :connected_at, metadata_fields])] } + + before do + allow(Gitlab::Kas::Client).to receive(:new).and_return(double(get_connected_agents: [connected_agent])) + end + + it 'can retrieve connections and agent metadata' do + post_graphql(query, current_user: current_user) + + connection = graphql_data_at(:project, :cluster_agents, :nodes, :connections, :nodes).first + + expect(connection).to include({ + 'connectionId' => connected_agent.connection_id.to_s, + 'connectedAt' => Time.at(connected_agent.connected_at), + 'metadata' => { + 'version' => agent_meta.version, + 'commit' => agent_meta.commit_id, + 'podNamespace' => agent_meta.pod_namespace, + 'podName' => agent_meta.pod_name + } + }) + end + end +end diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb index 3ad56223b61..692143b2215 100644 --- a/spec/requests/api/graphql/project/container_repositories_spec.rb +++ b/spec/requests/api/graphql/project/container_repositories_spec.rb @@ -12,11 +12,12 @@ RSpec.describe 'getting container repositories in a project' do let_it_be(:container_repositories) { [container_repository, container_repositories_delete_scheduled, container_repositories_delete_failed].flatten } let_it_be(:container_expiration_policy) { project.container_expiration_policy } + let(:excluded_fields) { %w[pipeline jobs] } let(:container_repositories_fields) do <<~GQL edges { node { - #{all_graphql_fields_for('container_repositories'.classify, excluded: %w(pipeline jobs))} + #{all_graphql_fields_for('container_repositories'.classify, excluded: excluded_fields)} } } GQL @@ -151,6 +152,12 @@ RSpec.describe 'getting container repositories in a project' do end end + it_behaves_like 'handling graphql network errors with the container registry' + + it_behaves_like 'not hitting graphql network errors with the container registry' do + let(:excluded_fields) { %w[pipeline jobs tags tagsCount] } + end + it 'returns the total count of container repositories' do subject @@ -190,7 +197,7 @@ RSpec.describe 'getting container repositories in a project' do it_behaves_like 'sorted paginated query' do let(:sort_param) { :NAME_ASC } let(:first_param) { 2 } - let(:expected_results) { [container_repository2.name, container_repository1.name, container_repository4.name, container_repository3.name, container_repository5.name] } + let(:all_records) { [container_repository2.name, container_repository1.name, container_repository4.name, container_repository3.name, container_repository5.name] } end end @@ -198,7 +205,7 @@ RSpec.describe 'getting container repositories in a project' do it_behaves_like 'sorted paginated query' do let(:sort_param) { :NAME_DESC } let(:first_param) { 2 } - let(:expected_results) { [container_repository5.name, container_repository3.name, container_repository4.name, container_repository1.name, container_repository2.name] } + let(:all_records) { [container_repository5.name, container_repository3.name, container_repository4.name, container_repository1.name, container_repository2.name] } end end end diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index c6b4d82bf15..1c6d6ce4707 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -5,12 +5,15 @@ require 'spec_helper' RSpec.describe 'getting an issue list for a project' do include GraphqlHelpers - let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :repository, :public, group: group) } let_it_be(:current_user) { create(:user) } let_it_be(:issue_a, reload: true) { create(:issue, project: project, discussion_locked: true) } let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project) } let_it_be(:issues, reload: true) { [issue_a, issue_b] } + let(:issue_a_gid) { issue_a.to_global_id.to_s } + let(:issue_b_gid) { issue_b.to_global_id.to_s } let(:issues_data) { graphql_data['project']['issues']['edges'] } let(:issue_filter_params) { {} } @@ -66,9 +69,6 @@ RSpec.describe 'getting an issue list for a project' do let_it_be(:upvote_award) { create(:award_emoji, :upvote, user: current_user, awardable: issue_a) } - let(:issue_a_gid) { issue_a.to_global_id.to_s } - let(:issue_b_gid) { issue_b.to_global_id.to_s } - where(:value, :gids) do 'thumbsup' | lazy { [issue_a_gid] } 'ANY' | lazy { [issue_a_gid] } @@ -84,7 +84,7 @@ RSpec.describe 'getting an issue list for a project' do it 'returns correctly filtered issues' do post_graphql(query, current_user: current_user) - expect(graphql_dig_at(issues_data, :node, :id)).to eq(gids) + expect(issues_ids).to eq(gids) end end end @@ -149,6 +149,8 @@ RSpec.describe 'getting an issue list for a project' do create(:issue, :confidential, project: project) end + let(:confidential_issue_gid) { confidential_issue.to_global_id.to_s } + context 'when the user cannot see confidential issues' do it 'returns issues without confidential issues' do post_graphql(query, current_user: current_user) @@ -159,12 +161,34 @@ RSpec.describe 'getting an issue list for a project' do expect(issue.dig('node', 'confidential')).to eq(false) end end + + context 'filtering for confidential issues' do + let(:issue_filter_params) { { confidential: true } } + + it 'returns no issues' do + post_graphql(query, current_user: current_user) + + expect(issues_data.size).to eq(0) + end + end + + context 'filtering for non-confidential issues' do + let(:issue_filter_params) { { confidential: false } } + + it 'returns correctly filtered issues' do + post_graphql(query, current_user: current_user) + + expect(issues_ids).to contain_exactly(issue_a_gid, issue_b_gid) + end + end end context 'when the user can see confidential issues' do - it 'returns issues with confidential issues' do + before do project.add_developer(current_user) + end + it 'returns issues with confidential issues' do post_graphql(query, current_user: current_user) expect(issues_data.size).to eq(3) @@ -175,6 +199,26 @@ RSpec.describe 'getting an issue list for a project' do expect(confidentials).to eq([true, false, false]) end + + context 'filtering for confidential issues' do + let(:issue_filter_params) { { confidential: true } } + + it 'returns correctly filtered issues' do + post_graphql(query, current_user: current_user) + + expect(issues_ids).to contain_exactly(confidential_issue_gid) + end + end + + context 'filtering for non-confidential issues' do + let(:issue_filter_params) { { confidential: false } } + + it 'returns correctly filtered issues' do + post_graphql(query, current_user: current_user) + + expect(issues_ids).to contain_exactly(issue_a_gid, issue_b_gid) + end + end end end @@ -205,7 +249,7 @@ RSpec.describe 'getting an issue list for a project' do it_behaves_like 'sorted paginated query' do let(:sort_param) { :DUE_DATE_ASC } let(:first_param) { 2 } - let(:expected_results) { [due_issue3.iid, due_issue5.iid, due_issue1.iid, due_issue4.iid, due_issue2.iid] } + let(:all_records) { [due_issue3.iid, due_issue5.iid, due_issue1.iid, due_issue4.iid, due_issue2.iid] } end end @@ -213,7 +257,7 @@ RSpec.describe 'getting an issue list for a project' do it_behaves_like 'sorted paginated query' do let(:sort_param) { :DUE_DATE_DESC } let(:first_param) { 2 } - let(:expected_results) { [due_issue1.iid, due_issue5.iid, due_issue3.iid, due_issue4.iid, due_issue2.iid] } + let(:all_records) { [due_issue1.iid, due_issue5.iid, due_issue3.iid, due_issue4.iid, due_issue2.iid] } end end end @@ -230,10 +274,10 @@ RSpec.describe 'getting an issue list for a project' do it_behaves_like 'sorted paginated query' do let(:sort_param) { :RELATIVE_POSITION_ASC } let(:first_param) { 2 } - let(:expected_results) do + let(:all_records) do [ relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, - relative_issue4.iid, relative_issue2.iid + relative_issue2.iid, relative_issue4.iid ] end end @@ -256,7 +300,7 @@ RSpec.describe 'getting an issue list for a project' do it_behaves_like 'sorted paginated query' do let(:sort_param) { :PRIORITY_ASC } let(:first_param) { 2 } - let(:expected_results) do + let(:all_records) do [ priority_issue3.iid, priority_issue1.iid, priority_issue2.iid, priority_issue4.iid @@ -269,7 +313,7 @@ RSpec.describe 'getting an issue list for a project' do it_behaves_like 'sorted paginated query' do let(:sort_param) { :PRIORITY_DESC } let(:first_param) { 2 } - let(:expected_results) do + let(:all_records) do [priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid] end end @@ -288,17 +332,17 @@ RSpec.describe 'getting an issue list for a project' do context 'when ascending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { :LABEL_PRIORITY_ASC } - let(:first_param) { 2 } - let(:expected_results) { [label_issue3.iid, label_issue1.iid, label_issue2.iid, label_issue4.iid] } + let(:sort_param) { :LABEL_PRIORITY_ASC } + let(:first_param) { 2 } + let(:all_records) { [label_issue3.iid, label_issue1.iid, label_issue2.iid, label_issue4.iid] } end end context 'when descending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { :LABEL_PRIORITY_DESC } - let(:first_param) { 2 } - let(:expected_results) { [label_issue2.iid, label_issue3.iid, label_issue1.iid, label_issue4.iid] } + let(:sort_param) { :LABEL_PRIORITY_DESC } + let(:first_param) { 2 } + let(:all_records) { [label_issue2.iid, label_issue3.iid, label_issue1.iid, label_issue4.iid] } end end end @@ -313,17 +357,17 @@ RSpec.describe 'getting an issue list for a project' do context 'when ascending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { :MILESTONE_DUE_ASC } - let(:first_param) { 2 } - let(:expected_results) { [milestone_issue2.iid, milestone_issue3.iid, milestone_issue1.iid] } + let(:sort_param) { :MILESTONE_DUE_ASC } + let(:first_param) { 2 } + let(:all_records) { [milestone_issue2.iid, milestone_issue3.iid, milestone_issue1.iid] } end end context 'when descending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { :MILESTONE_DUE_DESC } - let(:first_param) { 2 } - let(:expected_results) { [milestone_issue3.iid, milestone_issue2.iid, milestone_issue1.iid] } + let(:sort_param) { :MILESTONE_DUE_DESC } + let(:first_param) { 2 } + let(:all_records) { [milestone_issue3.iid, milestone_issue2.iid, milestone_issue1.iid] } end end end @@ -366,6 +410,35 @@ RSpec.describe 'getting an issue list for a project' do end end + context 'when fetching customer_relations_contacts' do + let(:fields) do + <<~QUERY + nodes { + id + customerRelationsContacts { + nodes { + firstName + } + } + } + QUERY + end + + def clean_state_query + run_with_clean_state(query, context: { current_user: current_user }) + end + + it 'avoids N+1 queries' do + create(:contact, group_id: group.id, issues: [issue_a]) + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) { clean_state_query } + + create(:contact, group_id: group.id, issues: [issue_a]) + + expect { clean_state_query }.not_to exceed_all_query_limit(control) + end + end + context 'when fetching labels' do let(:fields) do <<~QUERY @@ -526,4 +599,8 @@ RSpec.describe 'getting an issue list for a project' do include_examples 'N+1 query check' end end + + def issues_ids + graphql_dig_at(issues_data, :node, :id) + end end diff --git a/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb b/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb index 70c5bda35e1..820a5d818c7 100644 --- a/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb +++ b/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb @@ -39,7 +39,7 @@ RSpec.describe 'Query.project.mergeRequests.pipelines' do before do merge_requests.each do |mr| - shas = mr.all_commits.limit(2).pluck(:sha) + shas = mr.recent_diff_head_shas shas.each do |sha| create(:ci_pipeline, :success, project: project, ref: mr.source_branch, sha: sha) @@ -52,7 +52,7 @@ RSpec.describe 'Query.project.mergeRequests.pipelines' do p_nodes = graphql_data_at(:project, :merge_requests, :nodes) - expect(p_nodes).to all(match('iid' => be_present, 'pipelines' => match('count' => 2))) + expect(p_nodes).to all(match('iid' => be_present, 'pipelines' => match('count' => 1))) end it 'is scalable', :request_store, :use_clean_rails_memory_store_caching do diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb index 1b0405be09c..b0bedd99fce 100644 --- a/spec/requests/api/graphql/project/merge_requests_spec.rb +++ b/spec/requests/api/graphql/project/merge_requests_spec.rb @@ -385,7 +385,7 @@ RSpec.describe 'getting merge request listings nested in a project' do context 'when sorting by merged_at DESC' do let(:sort_param) { :MERGED_AT_DESC } - let(:expected_results) do + let(:all_records) do [ merge_request_b, merge_request_d, @@ -418,14 +418,14 @@ RSpec.describe 'getting merge request listings nested in a project' do query = pagination_query(params) post_graphql(query, current_user: current_user) - expect(results.map { |item| item["id"] }).to eq(expected_results.last(2)) + expect(results.map { |item| item["id"] }).to eq(all_records.last(2)) end end end context 'when sorting by closed_at DESC' do let(:sort_param) { :CLOSED_AT_DESC } - let(:expected_results) do + let(:all_records) do [ merge_request_b, merge_request_d, @@ -458,7 +458,7 @@ RSpec.describe 'getting merge request listings nested in a project' do query = pagination_query(params) post_graphql(query, current_user: current_user) - expect(results.map { |item| item["id"] }).to eq(expected_results.last(2)) + expect(results.map { |item| item["id"] }).to eq(all_records.last(2)) end end end diff --git a/spec/requests/api/graphql/project/releases_spec.rb b/spec/requests/api/graphql/project/releases_spec.rb index 8ccdb955ed9..2816ce90a6b 100644 --- a/spec/requests/api/graphql/project/releases_spec.rb +++ b/spec/requests/api/graphql/project/releases_spec.rb @@ -322,17 +322,17 @@ RSpec.describe 'Query.project(fullPath).releases()' do context 'when ascending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { :RELEASED_AT_ASC } - let(:first_param) { 2 } - let(:expected_results) { [release1.tag, release2.tag, release3.tag, release4.tag, release5.tag] } + let(:sort_param) { :RELEASED_AT_ASC } + let(:first_param) { 2 } + let(:all_records) { [release1.tag, release2.tag, release3.tag, release4.tag, release5.tag] } end end context 'when descending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { :RELEASED_AT_DESC } - let(:first_param) { 2 } - let(:expected_results) { [release5.tag, release4.tag, release3.tag, release2.tag, release1.tag] } + let(:sort_param) { :RELEASED_AT_DESC } + let(:first_param) { 2 } + let(:all_records) { [release5.tag, release4.tag, release3.tag, release2.tag, release1.tag] } end end end @@ -346,17 +346,17 @@ RSpec.describe 'Query.project(fullPath).releases()' do context 'when ascending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { :CREATED_ASC } - let(:first_param) { 2 } - let(:expected_results) { [release1.tag, release2.tag, release3.tag, release4.tag, release5.tag] } + let(:sort_param) { :CREATED_ASC } + let(:first_param) { 2 } + let(:all_records) { [release1.tag, release2.tag, release3.tag, release4.tag, release5.tag] } end end context 'when descending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { :CREATED_DESC } - let(:first_param) { 2 } - let(:expected_results) { [release5.tag, release4.tag, release3.tag, release2.tag, release1.tag] } + let(:sort_param) { :CREATED_DESC } + let(:first_param) { 2 } + let(:all_records) { [release5.tag, release4.tag, release3.tag, release2.tag, release1.tag] } end end end diff --git a/spec/requests/api/graphql/users_spec.rb b/spec/requests/api/graphql/users_spec.rb index 22b68fbc9bb..67cd35ee545 100644 --- a/spec/requests/api/graphql/users_spec.rb +++ b/spec/requests/api/graphql/users_spec.rb @@ -114,17 +114,17 @@ RSpec.describe 'Users' do context 'when ascending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { :CREATED_ASC } - let(:first_param) { 1 } - let(:expected_results) { ascending_users } + let(:sort_param) { :CREATED_ASC } + let(:first_param) { 1 } + let(:all_records) { ascending_users } end end context 'when descending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { :CREATED_DESC } - let(:first_param) { 1 } - let(:expected_results) { ascending_users.reverse } + let(:sort_param) { :CREATED_DESC } + let(:first_param) { 1 } + let(:all_records) { ascending_users.reverse } end end end diff --git a/spec/requests/api/group_container_repositories_spec.rb b/spec/requests/api/group_container_repositories_spec.rb index fdbf910e4bc..bf29bd91414 100644 --- a/spec/requests/api/group_container_repositories_spec.rb +++ b/spec/requests/api/group_container_repositories_spec.rb @@ -20,12 +20,14 @@ RSpec.describe API::GroupContainerRepositories do end let(:api_user) { reporter } + let(:params) { {} } before do group.add_reporter(reporter) group.add_guest(guest) stub_container_registry_config(enabled: true) + stub_container_registry_info root_repository test_repository @@ -35,10 +37,13 @@ RSpec.describe API::GroupContainerRepositories do let(:url) { "/groups/#{group.id}/registry/repositories" } let(:snowplow_gitlab_standard_context) { { user: api_user, namespace: group } } - subject { get api(url, api_user) } + subject { get api(url, api_user), params: params } it_behaves_like 'rejected container repository access', :guest, :forbidden it_behaves_like 'rejected container repository access', :anonymous, :not_found + it_behaves_like 'handling network errors with the container registry' do + let(:params) { { tags: true } } + end it_behaves_like 'returns repositories for allowed users', :reporter, 'group' do let(:object) { group } diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 38abedde7da..2c7e2ecff85 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -728,16 +728,16 @@ RSpec.describe API::Groups do end it 'avoids N+1 queries with project links' do - get api("/groups/#{group1.id}", admin) + get api("/groups/#{group1.id}", user1) control_count = ActiveRecord::QueryRecorder.new do - get api("/groups/#{group1.id}", admin) + get api("/groups/#{group1.id}", user1) end.count create(:project, namespace: group1) expect do - get api("/groups/#{group1.id}", admin) + get api("/groups/#{group1.id}", user1) end.not_to exceed_query_limit(control_count) end @@ -746,7 +746,7 @@ RSpec.describe API::Groups do create(:group_group_link, shared_group: group1, shared_with_group: create(:group)) control_count = ActiveRecord::QueryRecorder.new do - get api("/groups/#{group1.id}", admin) + get api("/groups/#{group1.id}", user1) end.count # setup "n" more shared groups @@ -755,7 +755,7 @@ RSpec.describe API::Groups do # test that no of queries for 1 shared group is same as for n shared groups expect do - get api("/groups/#{group1.id}", admin) + get api("/groups/#{group1.id}", user1) end.not_to exceed_query_limit(control_count) end end @@ -1179,6 +1179,20 @@ RSpec.describe API::Groups do expect(json_response.length).to eq(1) expect(json_response.first['name']).to eq(project1.name) end + + it 'avoids N+1 queries' do + get api("/groups/#{group1.id}/projects", user1) + + control_count = ActiveRecord::QueryRecorder.new do + get api("/groups/#{group1.id}/projects", user1) + end.count + + create(:project, namespace: group1) + + expect do + get api("/groups/#{group1.id}/projects", user1) + end.not_to exceed_query_limit(control_count) + end end context "when authenticated as admin" do @@ -1196,20 +1210,6 @@ RSpec.describe API::Groups do expect(response).to have_gitlab_http_status(:not_found) end - - it 'avoids N+1 queries' do - get api("/groups/#{group1.id}/projects", admin) - - control_count = ActiveRecord::QueryRecorder.new do - get api("/groups/#{group1.id}/projects", admin) - end.count - - create(:project, namespace: group1) - - expect do - get api("/groups/#{group1.id}/projects", admin) - end.not_to exceed_query_limit(control_count) - end end context 'when using group path in URL' do diff --git a/spec/requests/api/helm_packages_spec.rb b/spec/requests/api/helm_packages_spec.rb index 3236857c5fc..5212e225351 100644 --- a/spec/requests/api/helm_packages_spec.rb +++ b/spec/requests/api/helm_packages_spec.rb @@ -18,11 +18,11 @@ RSpec.describe API::HelmPackages do let_it_be(:other_package) { create(:npm_package, project: project) } describe 'GET /api/v4/projects/:id/packages/helm/:channel/index.yaml' do - let(:url) { "/projects/#{project_id}/packages/helm/stable/index.yaml" } + let(:project_id) { project.id } + let(:channel) { 'stable' } + let(:url) { "/projects/#{project_id}/packages/helm/#{channel}/index.yaml" } context 'with a project id' do - let(:project_id) { project.id } - it_behaves_like 'handling helm chart index requests' end @@ -31,6 +31,18 @@ RSpec.describe API::HelmPackages do it_behaves_like 'handling helm chart index requests' end + + context 'with dot in channel' do + let(:channel) { 'with.dot' } + + subject { get api(url) } + + before do + project.update!(visibility: 'public') + end + + it_behaves_like 'returning response status', :success + end end describe 'GET /api/v4/projects/:id/packages/helm/:channel/charts/:file_name.tgz' do diff --git a/spec/requests/api/integrations_spec.rb b/spec/requests/api/integrations_spec.rb new file mode 100644 index 00000000000..649647804c0 --- /dev/null +++ b/spec/requests/api/integrations_spec.rb @@ -0,0 +1,363 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe API::Integrations do + let_it_be(:user) { create(:user) } + let_it_be(:user2) { create(:user) } + + let_it_be(:project, reload: true) do + create(:project, creator_id: user.id, namespace: user.namespace) + end + + %w[integrations services].each do |endpoint| + describe "GET /projects/:id/#{endpoint}" do + it 'returns authentication error when unauthenticated' do + get api("/projects/#{project.id}/#{endpoint}") + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it "returns error when authenticated but user is not a project owner" do + project.add_developer(user2) + get api("/projects/#{project.id}/#{endpoint}", user2) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + context 'with integrations' do + let!(:active_integration) { create(:emails_on_push_integration, project: project, active: true) } + let!(:integration) { create(:custom_issue_tracker_integration, project: project, active: false) } + + it "returns a list of all active integrations" do + get api("/projects/#{project.id}/#{endpoint}", user) + + aggregate_failures 'expect successful response with all active integrations' do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(json_response.count).to eq(1) + expect(json_response.first['slug']).to eq('emails-on-push') + expect(response).to match_response_schema('public_api/v4/integrations') + end + end + end + end + + Integration.available_integration_names.each do |integration| + describe "PUT /projects/:id/#{endpoint}/#{integration.dasherize}" do + include_context integration + + it "updates #{integration} settings" do + put api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user), params: integration_attrs + + expect(response).to have_gitlab_http_status(:ok) + + current_integration = project.integrations.first + events = current_integration.event_names.empty? ? ["foo"].freeze : current_integration.event_names + query_strings = [] + events.each do |event| + query_strings << "#{event}=#{!current_integration[event]}" + end + query_strings = query_strings.join('&') + + put api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}?#{query_strings}", user), params: integration_attrs + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['slug']).to eq(dashed_integration) + events.each do |event| + next if event == "foo" + + expect(project.integrations.first[event]).not_to eq(current_integration[event]), + "expected #{!current_integration[event]} for event #{event} for #{endpoint} #{current_integration.title}, got #{current_integration[event]}" + end + end + + it "returns if required fields missing" do + required_attributes = integration_attrs_list.select do |attr| + integration_klass.validators_on(attr).any? do |v| + v.instance_of?(ActiveRecord::Validations::PresenceValidator) && + # exclude presence validators with conditional since those are not really required + ![:if, :unless].any? { |cond| v.options.include?(cond) } + end + end + + if required_attributes.empty? + expected_code = :ok + else + integration_attrs.delete(required_attributes.sample) + expected_code = :bad_request + end + + put api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user), params: integration_attrs + + expect(response).to have_gitlab_http_status(expected_code) + end + end + + describe "DELETE /projects/:id/#{endpoint}/#{integration.dasherize}" do + include_context integration + + before do + initialize_integration(integration) + end + + it "deletes #{integration}" do + delete api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user) + + expect(response).to have_gitlab_http_status(:no_content) + project.send(integration_method).reload + expect(project.send(integration_method).activated?).to be_falsey + end + end + + describe "GET /projects/:id/#{endpoint}/#{integration.dasherize}" do + include_context integration + + let!(:initialized_integration) { initialize_integration(integration, active: true) } + + let_it_be(:project2) do + create(:project, creator_id: user.id, namespace: user.namespace) + end + + def deactive_integration! + return initialized_integration.update!(active: false) unless initialized_integration.is_a?(::Integrations::Prometheus) + + # Integrations::Prometheus sets `#active` itself within a `before_save`: + initialized_integration.manual_configuration = false + initialized_integration.save! + end + + it 'returns authentication error when unauthenticated' do + get api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}") + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it "returns all properties of active integration #{integration}" do + get api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user) + + expect(initialized_integration).to be_active + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['properties'].keys).to match_array(integration_instance.api_field_names) + end + + it "returns all properties of inactive integration #{integration}" do + deactive_integration! + + get api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user) + + expect(initialized_integration).not_to be_active + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['properties'].keys).to match_array(integration_instance.api_field_names) + end + + it "returns not found if integration does not exist" do + get api("/projects/#{project2.id}/#{endpoint}/#{dashed_integration}", user) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Integration Not Found') + end + + it "returns not found if integration exists but is in `Project#disabled_integrations`" do + expect_next_found_instance_of(Project) do |project| + expect(project).to receive(:disabled_integrations).at_least(:once).and_return([integration]) + end + + get api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Integration Not Found') + end + + it "returns error when authenticated but not a project owner" do + project.add_developer(user2) + get api("/projects/#{project.id}/#{endpoint}/#{dashed_integration}", user2) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + describe "POST /projects/:id/#{endpoint}/:slug/trigger" do + describe 'Mattermost integration' do + let(:integration_name) { 'mattermost_slash_commands' } + + context 'when no integration is available' do + it 'returns a not found message' do + post api("/projects/#{project.id}/#{endpoint}/idonotexist/trigger") + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response["error"]).to eq("404 Not Found") + end + end + + context 'when the integration exists' do + let(:params) { { token: 'token' } } + + context 'when the integration is not active' do + before do + project.create_mattermost_slash_commands_integration( + active: false, + properties: params + ) + end + + it 'when the integration is inactive' do + post api("/projects/#{project.id}/#{endpoint}/#{integration_name}/trigger"), params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when the integration is active' do + before do + project.create_mattermost_slash_commands_integration( + active: true, + properties: params + ) + end + + it 'returns status 200' do + post api("/projects/#{project.id}/#{endpoint}/#{integration_name}/trigger"), params: params + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when the project can not be found' do + it 'returns a generic 404' do + post api("/projects/404/#{endpoint}/#{integration_name}/trigger"), params: params + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response["message"]).to eq("404 Integration Not Found") + end + end + end + end + + describe 'Slack Integration' do + let(:integration_name) { 'slack_slash_commands' } + + before do + project.create_slack_slash_commands_integration( + active: true, + properties: { token: 'token' } + ) + end + + it 'returns status 200' do + post api("/projects/#{project.id}/#{endpoint}/#{integration_name}/trigger"), params: { token: 'token', text: 'help' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['response_type']).to eq("ephemeral") + end + end + end + + describe 'Mattermost integration' do + let(:integration_name) { 'mattermost' } + let(:params) do + { webhook: 'https://hook.example.com', username: 'username' } + end + + before do + project.create_mattermost_integration( + active: true, + properties: params + ) + end + + it 'accepts a username for update' do + put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), params: params.merge(username: 'new_username') + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['properties']['username']).to eq('new_username') + end + end + + describe 'Microsoft Teams integration' do + let(:integration_name) { 'microsoft-teams' } + let(:params) do + { + webhook: 'https://hook.example.com', + branches_to_be_notified: 'default', + notify_only_broken_pipelines: false + } + end + + before do + project.create_microsoft_teams_integration( + active: true, + properties: params + ) + end + + it 'accepts branches_to_be_notified for update' do + put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), + params: params.merge(branches_to_be_notified: 'all') + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['properties']['branches_to_be_notified']).to eq('all') + end + + it 'accepts notify_only_broken_pipelines for update' do + put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), + params: params.merge(notify_only_broken_pipelines: true) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['properties']['notify_only_broken_pipelines']).to eq(true) + end + end + + describe 'Hangouts Chat integration' do + let(:integration_name) { 'hangouts-chat' } + let(:params) do + { + webhook: 'https://hook.example.com', + branches_to_be_notified: 'default' + } + end + + before do + project.create_hangouts_chat_integration( + active: true, + properties: params + ) + end + + it 'accepts branches_to_be_notified for update', :aggregate_failures do + put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), params: params.merge(branches_to_be_notified: 'all') + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['properties']['branches_to_be_notified']).to eq('all') + end + + it 'only requires the webhook param' do + put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), params: { webhook: 'https://hook.example.com' } + + expect(response).to have_gitlab_http_status(:ok) + end + end + + describe 'Pipelines Email Integration' do + let(:integration_name) { 'pipelines-email' } + + context 'notify_only_broken_pipelines property was saved as a string' do + before do + project.create_pipelines_email_integration( + active: false, + properties: { + "notify_only_broken_pipelines": "true", + "branches_to_be_notified": "default" + } + ) + end + + it 'returns boolean values for notify_only_broken_pipelines' do + get api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user) + + expect(json_response['properties']['notify_only_broken_pipelines']).to eq(true) + end + end + end + end +end diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index 49756df61c6..aeca4e435f4 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -609,7 +609,7 @@ RSpec.describe API::Internal::Base do end context 'with Project' do - it_behaves_like 'storing arguments in the application context' do + it_behaves_like 'storing arguments in the application context for the API' do let(:expected_params) { { user: key.user.username, project: project.full_path, caller_id: "POST /api/:version/internal/allowed" } } subject { push(key, project) } @@ -617,7 +617,7 @@ RSpec.describe API::Internal::Base do end context 'with PersonalSnippet' do - it_behaves_like 'storing arguments in the application context' do + it_behaves_like 'storing arguments in the application context for the API' do let(:expected_params) { { user: key.user.username, caller_id: "POST /api/:version/internal/allowed" } } subject { push(key, personal_snippet) } @@ -625,7 +625,7 @@ RSpec.describe API::Internal::Base do end context 'with ProjectSnippet' do - it_behaves_like 'storing arguments in the application context' do + it_behaves_like 'storing arguments in the application context for the API' do let(:expected_params) { { user: key.user.username, project: project_snippet.project.full_path, caller_id: "POST /api/:version/internal/allowed" } } subject { push(key, project_snippet) } @@ -1197,7 +1197,7 @@ RSpec.describe API::Internal::Base do subject end - it_behaves_like 'storing arguments in the application context' do + it_behaves_like 'storing arguments in the application context for the API' do let(:expected_params) { expected_context } end end diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb index 24422f7b0dd..245e4e6ba15 100644 --- a/spec/requests/api/internal/kubernetes_spec.rb +++ b/spec/requests/api/internal/kubernetes_spec.rb @@ -177,94 +177,4 @@ RSpec.describe API::Internal::Kubernetes do end end end - - describe 'GET /internal/kubernetes/project_info' do - def send_request(headers: {}, params: {}) - get api('/internal/kubernetes/project_info'), params: params, headers: headers.reverse_merge(jwt_auth_headers) - end - - include_examples 'authorization' - include_examples 'agent authentication' - - context 'an agent is found' do - let_it_be(:agent_token) { create(:cluster_agent_token) } - - shared_examples 'agent token tracking' - - context 'project is public' do - let(:project) { create(:project, :public) } - - it 'returns expected data', :aggregate_failures do - send_request(params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }) - - expect(response).to have_gitlab_http_status(:success) - - expect(json_response).to match( - a_hash_including( - 'project_id' => project.id, - 'gitaly_info' => a_hash_including( - 'address' => match(/\.socket$/), - 'token' => 'secret', - 'features' => {} - ), - 'gitaly_repository' => a_hash_including( - 'storage_name' => project.repository_storage, - 'relative_path' => project.disk_path + '.git', - 'gl_repository' => "project-#{project.id}", - 'gl_project_path' => project.full_path - ) - ) - ) - end - - context 'repository is for project members only' do - let(:project) { create(:project, :public, :repository_private) } - - it 'returns 404' do - send_request(params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - context 'project is private' do - let(:project) { create(:project, :private) } - - it 'returns 404' do - send_request(params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }) - - expect(response).to have_gitlab_http_status(:not_found) - end - - context 'and agent belongs to project' do - let(:agent_token) { create(:cluster_agent_token, agent: create(:cluster_agent, project: project)) } - - it 'returns 200' do - send_request(params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }) - - expect(response).to have_gitlab_http_status(:success) - end - end - end - - context 'project is internal' do - let(:project) { create(:project, :internal) } - - it 'returns 404' do - send_request(params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'project does not exist' do - it 'returns 404' do - send_request(params: { id: non_existing_record_id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - end end diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb index 8a33e63b80b..9204ee4d7f0 100644 --- a/spec/requests/api/issues/issues_spec.rb +++ b/spec/requests/api/issues/issues_spec.rb @@ -138,6 +138,12 @@ RSpec.describe API::Issues do expect(json_response).to be_an Array end + it_behaves_like 'issuable anonymous search' do + let(:url) { '/issues' } + let(:issuable) { issue } + let(:result) { issuable.id } + end + it 'returns authentication error without any scope' do get api('/issues') @@ -256,6 +262,38 @@ RSpec.describe API::Issues do it_behaves_like 'issues statistics' end + + context 'with search param' do + let(:params) { { scope: 'all', search: 'foo' } } + let(:counts) { { all: 1, closed: 0, opened: 1 } } + + it_behaves_like 'issues statistics' + + context 'with anonymous user' do + let(:user) { nil } + + context 'with disable_anonymous_search disabled' do + before do + stub_feature_flags(disable_anonymous_search: false) + end + + it_behaves_like 'issues statistics' + end + + context 'with disable_anonymous_search enabled' do + before do + stub_feature_flags(disable_anonymous_search: true) + end + + it 'returns a unprocessable entity 422' do + get api("/issues_statistics"), params: params + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response['message']).to include('User must be authenticated to use search') + end + end + end + end end end diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb index 9d3bd26a200..82692366589 100644 --- a/spec/requests/api/issues/post_projects_issues_spec.rb +++ b/spec/requests/api/issues/post_projects_issues_spec.rb @@ -8,15 +8,15 @@ RSpec.describe API::Issues do create(:project, :public, creator_id: user.id, namespace: user.namespace) end - let(:user2) { create(:user) } - let(:non_member) { create(:user) } - let_it_be(:guest) { create(:user) } - let_it_be(:author) { create(:author) } - let_it_be(:assignee) { create(:assignee) } - let(:admin) { create(:user, :admin) } - let(:issue_title) { 'foo' } - let(:issue_description) { 'closed' } - let!(:closed_issue) do + let_it_be(:user2) { create(:user) } + let_it_be(:non_member) { create(:user) } + let_it_be(:guest) { create(:user) } + let_it_be(:author) { create(:author) } + let_it_be(:milestone) { create(:milestone, title: '1.0.0', project: project) } + let_it_be(:assignee) { create(:assignee) } + let_it_be(:admin) { create(:user, :admin) } + + let_it_be(:closed_issue) do create :closed_issue, author: user, assignees: [user], @@ -28,7 +28,7 @@ RSpec.describe API::Issues do closed_at: 1.hour.ago end - let!(:confidential_issue) do + let_it_be(:confidential_issue) do create :issue, :confidential, project: project, @@ -38,7 +38,7 @@ RSpec.describe API::Issues do updated_at: 2.hours.ago end - let!(:issue) do + let_it_be(:issue) do create :issue, author: user, assignees: [user], @@ -46,22 +46,21 @@ RSpec.describe API::Issues do milestone: milestone, created_at: generate(:past_time), updated_at: 1.hour.ago, - title: issue_title, - description: issue_description + title: 'foo', + description: 'closed' end + let_it_be(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } + let_it_be(:label) do create(:label, title: 'label', color: '#FFAABB', project: project) end let!(:label_link) { create(:label_link, label: label, target: issue) } - let(:milestone) { create(:milestone, title: '1.0.0', project: project) } let_it_be(:empty_milestone) do create(:milestone, title: '2.0.0', project: project) end - let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } - let(:no_milestone_title) { 'None' } let(:any_milestone_title) { 'Any' } @@ -400,16 +399,15 @@ RSpec.describe API::Issues do end context 'when request exceeds the rate limit' do - before do + it 'prevents users from creating more issues' do allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true) - end - it 'prevents users from creating more issues' do post api("/projects/#{project.id}/issues", user), params: { title: 'new issue', labels: 'label, label2', weight: 3, assignee_ids: [user2.id] } - expect(response).to have_gitlab_http_status(:too_many_requests) expect(json_response['message']['error']).to eq('This endpoint has been requested too many times. Try again later.') + + expect(response).to have_gitlab_http_status(:too_many_requests) end end end @@ -517,7 +515,7 @@ RSpec.describe API::Issues do end context 'when using the issue ID instead of iid' do - it 'returns 404 when trying to move an issue' do + it 'returns 404 when trying to move an issue', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/341520' do post api("/projects/#{project.id}/issues/#{issue.id}/move", user), params: { to_project_id: target_project.id } @@ -556,6 +554,114 @@ RSpec.describe API::Issues do end end + describe '/projects/:id/issues/:issue_iid/clone' do + let_it_be(:valid_target_project) { create(:project) } + let_it_be(:invalid_target_project) { create(:project) } + + before_all do + valid_target_project.add_maintainer(user) + end + + context 'when user can admin the issue' do + context 'when the user can admin the target project' do + it 'clones the issue' do + expect do + post_clone_issue(user, issue, valid_target_project) + end.to change { valid_target_project.issues.count }.by(1) + + cloned_issue = Issue.last + + expect(cloned_issue.notes.count).to eq(2) + expect(cloned_issue.notes.pluck(:note)).not_to include(issue.notes.first.note) + expect(response).to have_gitlab_http_status(:created) + expect(json_response['id']).to eq(cloned_issue.id) + expect(json_response['project_id']).to eq(valid_target_project.id) + end + + context 'when target project is the same source project' do + it 'clones the issue' do + expect do + post_clone_issue(user, issue, issue.project) + end.to change { issue.reset.project.issues.count }.by(1) + + cloned_issue = Issue.last + + expect(cloned_issue.notes.count).to eq(2) + expect(cloned_issue.notes.pluck(:note)).not_to include(issue.notes.first.note) + expect(response).to have_gitlab_http_status(:created) + expect(json_response['id']).to eq(cloned_issue.id) + expect(json_response['project_id']).to eq(issue.project.id) + end + end + end + end + + context 'when the user does not have the permission to clone issues' do + it 'returns 400' do + post api("/projects/#{project.id}/issues/#{issue.iid}/clone", user), + params: { to_project_id: invalid_target_project.id } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq(s_('CloneIssue|Cannot clone issue due to insufficient permissions!')) + end + end + + context 'when using the issue ID instead of iid' do + it 'returns 404', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/341520' do + post api("/projects/#{project.id}/issues/#{issue.id}/clone", user), + params: { to_project_id: valid_target_project.id } + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Issue Not Found') + end + end + + context 'when issue does not exist' do + it 'returns 404' do + post api("/projects/#{project.id}/issues/12300/clone", user), + params: { to_project_id: valid_target_project.id } + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Issue Not Found') + end + end + + context 'when source project does not exist' do + it 'returns 404' do + post api("/projects/0/issues/#{issue.iid}/clone", user), + params: { to_project_id: valid_target_project.id } + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Project Not Found') + end + end + + context 'when target project does not exist' do + it 'returns 404' do + post api("/projects/#{project.id}/issues/#{issue.iid}/clone", user), + params: { to_project_id: 0 } + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Project Not Found') + end + end + + it 'clones the issue with notes when with_notes is true' do + expect do + post api("/projects/#{project.id}/issues/#{issue.iid}/clone", user), + params: { to_project_id: valid_target_project.id, with_notes: true } + end.to change { valid_target_project.issues.count }.by(1) + + cloned_issue = Issue.last + + expect(cloned_issue.notes.count).to eq(3) + expect(cloned_issue.notes.pluck(:note)).to include(issue.notes.first.note) + expect(response).to have_gitlab_http_status(:created) + expect(json_response['id']).to eq(cloned_issue.id) + expect(json_response['project_id']).to eq(valid_target_project.id) + end + end + describe 'POST :id/issues/:issue_iid/subscribe' do it 'subscribes to an issue' do post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user2) @@ -576,7 +682,7 @@ RSpec.describe API::Issues do expect(response).to have_gitlab_http_status(:not_found) end - it 'returns 404 if the issue ID is used instead of the iid' do + it 'returns 404 if the issue ID is used instead of the iid', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/341520' do post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user) expect(response).to have_gitlab_http_status(:not_found) @@ -609,7 +715,7 @@ RSpec.describe API::Issues do expect(response).to have_gitlab_http_status(:not_found) end - it 'returns 404 if using the issue ID instead of iid' do + it 'returns 404 if using the issue ID instead of iid', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/341520' do post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user) expect(response).to have_gitlab_http_status(:not_found) @@ -621,4 +727,9 @@ RSpec.describe API::Issues do expect(response).to have_gitlab_http_status(:not_found) end end + + def post_clone_issue(current_user, issue, target_project) + post api("/projects/#{issue.project.id}/issues/#{issue.iid}/clone", current_user), + params: { to_project_id: target_project.id } + end end diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb index 07111dd1d62..5a682ee8532 100644 --- a/spec/requests/api/maven_packages_spec.rb +++ b/spec/requests/api/maven_packages_spec.rb @@ -798,8 +798,6 @@ RSpec.describe API::MavenPackages do end describe 'PUT /api/v4/projects/:id/packages/maven/*path/:file_name' do - include_context 'workhorse headers' - let(:send_rewritten_field) { true } let(:file_upload) { fixture_file_upload('spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.jar') } @@ -833,6 +831,8 @@ RSpec.describe API::MavenPackages do context 'when params from workhorse are correct' do let(:params) { { file: file_upload } } + subject { upload_file_with_token(params: params) } + context 'file size is too large' do it 'rejects the request' do allow_next_instance_of(UploadedFile) do |uploaded_file| @@ -851,18 +851,20 @@ RSpec.describe API::MavenPackages do expect(response).to have_gitlab_http_status(:bad_request) end - context 'without workhorse header' do - let(:workhorse_headers) { {} } - - subject { upload_file_with_token(params: params) } - - it_behaves_like 'package workhorse uploads' - end + it_behaves_like 'package workhorse uploads' context 'event tracking' do - subject { upload_file_with_token(params: params) } - it_behaves_like 'a package tracking event', described_class.name, 'push_package' + + context 'when the package file fails to be created' do + before do + allow_next_instance_of(::Packages::CreatePackageFileService) do |create_package_file_service| + allow(create_package_file_service).to receive(:execute).and_raise(StandardError) + end + end + + it_behaves_like 'not a package tracking event' + end end it 'creates package and stores package file' do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 7a587e82683..bdbc73a59d8 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -49,6 +49,12 @@ RSpec.describe API::MergeRequests do expect_successful_response_with_paginated_array end + + it_behaves_like 'issuable anonymous search' do + let(:url) { endpoint_path } + let(:issuable) { merge_request } + let(:result) { [merge_request_merged.id, merge_request_locked.id, merge_request_closed.id, merge_request.id] } + end end context 'when authenticated' do @@ -613,6 +619,12 @@ RSpec.describe API::MergeRequests do ) end + it_behaves_like 'issuable anonymous search' do + let(:url) { '/merge_requests' } + let(:issuable) { merge_request } + let(:result) { [merge_request_merged.id, merge_request_locked.id, merge_request_closed.id, merge_request.id] } + end + it "returns authentication error without any scope" do get api("/merge_requests") diff --git a/spec/requests/api/package_files_spec.rb b/spec/requests/api/package_files_spec.rb index 137ded050c5..eb1f04d193e 100644 --- a/spec/requests/api/package_files_spec.rb +++ b/spec/requests/api/package_files_spec.rb @@ -38,7 +38,7 @@ RSpec.describe API::PackageFiles do expect(response).to have_gitlab_http_status(:not_found) end - it 'returns 404 for a user without access to the project' do + it 'returns 404 for a user without access to the project', :sidekiq_inline do project.team.truncate get api(url, user) diff --git a/spec/requests/api/project_container_repositories_spec.rb b/spec/requests/api/project_container_repositories_spec.rb index 1170a9ba6cb..196b0395ec0 100644 --- a/spec/requests/api/project_container_repositories_spec.rb +++ b/spec/requests/api/project_container_repositories_spec.rb @@ -52,6 +52,7 @@ RSpec.describe API::ProjectContainerRepositories do test_repository stub_container_registry_config(enabled: true) + stub_container_registry_info end shared_context 'using API user' do @@ -105,6 +106,9 @@ RSpec.describe API::ProjectContainerRepositories do it_behaves_like 'rejected container repository access', :guest, :forbidden unless context == 'using job token' it_behaves_like 'rejected container repository access', :anonymous, :not_found it_behaves_like 'a package tracking event', described_class.name, 'list_repositories' + it_behaves_like 'handling network errors with the container registry' do + let(:params) { { tags: true } } + end it_behaves_like 'returns repositories for allowed users', :reporter, 'project' do let(:object) { project } @@ -154,6 +158,7 @@ RSpec.describe API::ProjectContainerRepositories do it_behaves_like 'rejected container repository access', :guest, :forbidden unless context == 'using job token' it_behaves_like 'rejected container repository access', :anonymous, :not_found + it_behaves_like 'handling network errors with the container registry' context 'for reporter' do let(:api_user) { reporter } diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb index 06f4475ef79..b9c458373a8 100644 --- a/spec/requests/api/project_export_spec.rb +++ b/spec/requests/api/project_export_spec.rb @@ -457,4 +457,143 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache do end end end + + describe 'export relations' do + let(:relation) { 'labels' } + let(:download_path) { "/projects/#{project.id}/export_relations/download?relation=#{relation}" } + let(:path) { "/projects/#{project.id}/export_relations" } + + let_it_be(:status_path) { "/projects/#{project.id}/export_relations/status" } + + context 'when user is a maintainer' do + before do + project.add_maintainer(user) + end + + describe 'POST /projects/:id/export_relations' do + it 'accepts the request' do + post api(path, user) + + expect(response).to have_gitlab_http_status(:accepted) + end + + context 'when response is not success' do + it 'returns api error' do + allow_next_instance_of(BulkImports::ExportService) do |service| + allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'error', http_status: :error)) + end + + post api(path, user) + + expect(response).to have_gitlab_http_status(:error) + end + end + end + + describe 'GET /projects/:id/export_relations/download' do + let_it_be(:export) { create(:bulk_import_export, project: project, relation: 'labels') } + let_it_be(:upload) { create(:bulk_import_export_upload, export: export) } + + context 'when export file exists' do + it 'downloads exported project relation archive' do + upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/gz/labels.ndjson.gz')) + + get api(download_path, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.header['Content-Disposition']).to eq("attachment; filename=\"labels.ndjson.gz\"; filename*=UTF-8''labels.ndjson.gz") + end + end + + context 'when relation is not portable' do + let(:relation) { ::BulkImports::FileTransfer::ProjectConfig.new(project).skipped_relations.first } + + it_behaves_like '400 response' do + let(:request) { get api(download_path, user) } + end + end + + context 'when export file does not exist' do + it 'returns 404' do + allow(upload).to receive(:export_file).and_return(nil) + + get api(download_path, user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + describe 'GET /projects/:id/export_relations/status' do + it 'returns a list of relation export statuses' do + create(:bulk_import_export, :started, project: project, relation: 'labels') + create(:bulk_import_export, :finished, project: project, relation: 'milestones') + create(:bulk_import_export, :failed, project: project, relation: 'project_badges') + + get api(status_path, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.pluck('relation')).to contain_exactly('labels', 'milestones', 'project_badges') + expect(json_response.pluck('status')).to contain_exactly(-1, 0, 1) + end + end + + context 'with bulk_import FF disabled' do + before do + stub_feature_flags(bulk_import: false) + end + + describe 'POST /projects/:id/export_relations' do + it_behaves_like '404 response' do + let(:request) { post api(path, user) } + end + end + + describe 'GET /projects/:id/export_relations/download' do + let_it_be(:export) { create(:bulk_import_export, project: project, relation: 'labels') } + let_it_be(:upload) { create(:bulk_import_export_upload, export: export) } + + before do + upload.update!(export_file: fixture_file_upload('spec/fixtures/bulk_imports/gz/labels.ndjson.gz')) + end + + it_behaves_like '404 response' do + let(:request) { post api(path, user) } + end + end + + describe 'GET /projects/:id/export_relations/status' do + it_behaves_like '404 response' do + let(:request) { get api(status_path, user) } + end + end + end + end + + context 'when user is a developer' do + let_it_be(:developer) { create(:user) } + + before do + project.add_developer(developer) + end + + describe 'POST /projects/:id/export_relations' do + it_behaves_like '403 response' do + let(:request) { post api(path, developer) } + end + end + + describe 'GET /projects/:id/export_relations/download' do + it_behaves_like '403 response' do + let(:request) { get api(download_path, developer) } + end + end + + describe 'GET /projects/:id/export_relations/status' do + it_behaves_like '403 response' do + let(:request) { get api(status_path, developer) } + end + end + end + end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index be8a6c7bdcf..b5d3dcee804 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -2591,7 +2591,7 @@ RSpec.describe API::Projects do end end - it_behaves_like 'storing arguments in the application context' do + it_behaves_like 'storing arguments in the application context for the API' do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :public) } let(:expected_params) { { user: user.username, project: project.full_path } } @@ -2684,26 +2684,9 @@ RSpec.describe API::Projects do context 'when authenticated' do context 'valid request' do - context 'when sort_by_project_authorizations_user_id FF is off' do - before do - stub_feature_flags(sort_by_project_users_by_project_authorizations_user_id: false) - end - - it_behaves_like 'project users response' do - let(:project) { project4 } - let(:current_user) { user4 } - end - end - - context 'when sort_by_project_authorizations_user_id FF is on' do - before do - stub_feature_flags(sort_by_project_users_by_project_authorizations_user_id: true) - end - - it_behaves_like 'project users response' do - let(:project) { project4 } - let(:current_user) { user4 } - end + it_behaves_like 'project users response' do + let(:project) { project4 } + let(:current_user) { user4 } end end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index a576e1ab1ee..f05f125c974 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -305,6 +305,18 @@ RSpec.describe API::Repositories do end end + it 'returns only a part of the repository with path set' do + path = 'bar' + get api("#{route}?path=#{path}", current_user) + + expect(response).to have_gitlab_http_status(:ok) + + type, params = workhorse_send_data + + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{project.path}\-[^\.]+\-#{path}\.tar.gz/) + end + it 'rate limits user when thresholds hit' do allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true) diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb deleted file mode 100644 index e550132e776..00000000000 --- a/spec/requests/api/services_spec.rb +++ /dev/null @@ -1,361 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe API::Services do - let_it_be(:user) { create(:user) } - let_it_be(:user2) { create(:user) } - - let_it_be(:project, reload: true) do - create(:project, creator_id: user.id, namespace: user.namespace) - end - - describe "GET /projects/:id/services" do - it 'returns authentication error when unauthenticated' do - get api("/projects/#{project.id}/services") - - expect(response).to have_gitlab_http_status(:unauthorized) - end - - it "returns error when authenticated but user is not a project owner" do - project.add_developer(user2) - get api("/projects/#{project.id}/services", user2) - - expect(response).to have_gitlab_http_status(:forbidden) - end - - context 'with integrations' do - let!(:active_integration) { create(:emails_on_push_integration, project: project, active: true) } - let!(:integration) { create(:custom_issue_tracker_integration, project: project, active: false) } - - it "returns a list of all active integrations" do - get api("/projects/#{project.id}/services", user) - - aggregate_failures 'expect successful response with all active integrations' do - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_an Array - expect(json_response.count).to eq(1) - expect(json_response.first['slug']).to eq('emails-on-push') - expect(response).to match_response_schema('public_api/v4/services') - end - end - end - end - - Integration.available_integration_names.each do |integration| - describe "PUT /projects/:id/services/#{integration.dasherize}" do - include_context integration - - it "updates #{integration} settings" do - put api("/projects/#{project.id}/services/#{dashed_integration}", user), params: integration_attrs - - expect(response).to have_gitlab_http_status(:ok) - - current_integration = project.integrations.first - events = current_integration.event_names.empty? ? ["foo"].freeze : current_integration.event_names - query_strings = [] - events.each do |event| - query_strings << "#{event}=#{!current_integration[event]}" - end - query_strings = query_strings.join('&') - - put api("/projects/#{project.id}/services/#{dashed_integration}?#{query_strings}", user), params: integration_attrs - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['slug']).to eq(dashed_integration) - events.each do |event| - next if event == "foo" - - expect(project.integrations.first[event]).not_to eq(current_integration[event]), - "expected #{!current_integration[event]} for event #{event} for service #{current_integration.title}, got #{current_integration[event]}" - end - end - - it "returns if required fields missing" do - required_attributes = integration_attrs_list.select do |attr| - integration_klass.validators_on(attr).any? do |v| - v.instance_of?(ActiveRecord::Validations::PresenceValidator) && - # exclude presence validators with conditional since those are not really required - ![:if, :unless].any? { |cond| v.options.include?(cond) } - end - end - - if required_attributes.empty? - expected_code = :ok - else - integration_attrs.delete(required_attributes.sample) - expected_code = :bad_request - end - - put api("/projects/#{project.id}/services/#{dashed_integration}", user), params: integration_attrs - - expect(response).to have_gitlab_http_status(expected_code) - end - end - - describe "DELETE /projects/:id/services/#{integration.dasherize}" do - include_context integration - - before do - initialize_integration(integration) - end - - it "deletes #{integration}" do - delete api("/projects/#{project.id}/services/#{dashed_integration}", user) - - expect(response).to have_gitlab_http_status(:no_content) - project.send(integration_method).reload - expect(project.send(integration_method).activated?).to be_falsey - end - end - - describe "GET /projects/:id/services/#{integration.dasherize}" do - include_context integration - - let!(:initialized_integration) { initialize_integration(integration, active: true) } - - let_it_be(:project2) do - create(:project, creator_id: user.id, namespace: user.namespace) - end - - def deactive_integration! - return initialized_integration.update!(active: false) unless initialized_integration.is_a?(::Integrations::Prometheus) - - # Integrations::Prometheus sets `#active` itself within a `before_save`: - initialized_integration.manual_configuration = false - initialized_integration.save! - end - - it 'returns authentication error when unauthenticated' do - get api("/projects/#{project.id}/services/#{dashed_integration}") - expect(response).to have_gitlab_http_status(:unauthorized) - end - - it "returns all properties of active service #{integration}" do - get api("/projects/#{project.id}/services/#{dashed_integration}", user) - - expect(initialized_integration).to be_active - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['properties'].keys).to match_array(integration_instance.api_field_names) - end - - it "returns all properties of inactive integration #{integration}" do - deactive_integration! - - get api("/projects/#{project.id}/services/#{dashed_integration}", user) - - expect(initialized_integration).not_to be_active - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['properties'].keys).to match_array(integration_instance.api_field_names) - end - - it "returns not found if integration does not exist" do - get api("/projects/#{project2.id}/services/#{dashed_integration}", user) - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq('404 Service Not Found') - end - - it "returns not found if service exists but is in `Project#disabled_integrations`" do - expect_next_found_instance_of(Project) do |project| - expect(project).to receive(:disabled_integrations).at_least(:once).and_return([integration]) - end - - get api("/projects/#{project.id}/services/#{dashed_integration}", user) - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq('404 Service Not Found') - end - - it "returns error when authenticated but not a project owner" do - project.add_developer(user2) - get api("/projects/#{project.id}/services/#{dashed_integration}", user2) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - end - - describe 'POST /projects/:id/services/:slug/trigger' do - describe 'Mattermost integration' do - let(:integration_name) { 'mattermost_slash_commands' } - - context 'when no integration is available' do - it 'returns a not found message' do - post api("/projects/#{project.id}/services/idonotexist/trigger") - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response["error"]).to eq("404 Not Found") - end - end - - context 'when the integration exists' do - let(:params) { { token: 'token' } } - - context 'when the integration is not active' do - before do - project.create_mattermost_slash_commands_integration( - active: false, - properties: params - ) - end - - it 'when the integration is inactive' do - post api("/projects/#{project.id}/services/#{integration_name}/trigger"), params: params - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'when the integration is active' do - before do - project.create_mattermost_slash_commands_integration( - active: true, - properties: params - ) - end - - it 'returns status 200' do - post api("/projects/#{project.id}/services/#{integration_name}/trigger"), params: params - - expect(response).to have_gitlab_http_status(:ok) - end - end - - context 'when the project can not be found' do - it 'returns a generic 404' do - post api("/projects/404/services/#{integration_name}/trigger"), params: params - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response["message"]).to eq("404 Service Not Found") - end - end - end - end - - describe 'Slack Integration' do - let(:integration_name) { 'slack_slash_commands' } - - before do - project.create_slack_slash_commands_integration( - active: true, - properties: { token: 'token' } - ) - end - - it 'returns status 200' do - post api("/projects/#{project.id}/services/#{integration_name}/trigger"), params: { token: 'token', text: 'help' } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['response_type']).to eq("ephemeral") - end - end - end - - describe 'Mattermost integration' do - let(:integration_name) { 'mattermost' } - let(:params) do - { webhook: 'https://hook.example.com', username: 'username' } - end - - before do - project.create_mattermost_integration( - active: true, - properties: params - ) - end - - it 'accepts a username for update' do - put api("/projects/#{project.id}/services/#{integration_name}", user), params: params.merge(username: 'new_username') - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['properties']['username']).to eq('new_username') - end - end - - describe 'Microsoft Teams integration' do - let(:integration_name) { 'microsoft-teams' } - let(:params) do - { - webhook: 'https://hook.example.com', - branches_to_be_notified: 'default', - notify_only_broken_pipelines: false - } - end - - before do - project.create_microsoft_teams_integration( - active: true, - properties: params - ) - end - - it 'accepts branches_to_be_notified for update' do - put api("/projects/#{project.id}/services/#{integration_name}", user), - params: params.merge(branches_to_be_notified: 'all') - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['properties']['branches_to_be_notified']).to eq('all') - end - - it 'accepts notify_only_broken_pipelines for update' do - put api("/projects/#{project.id}/services/#{integration_name}", user), - params: params.merge(notify_only_broken_pipelines: true) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['properties']['notify_only_broken_pipelines']).to eq(true) - end - end - - describe 'Hangouts Chat integration' do - let(:integration_name) { 'hangouts-chat' } - let(:params) do - { - webhook: 'https://hook.example.com', - branches_to_be_notified: 'default' - } - end - - before do - project.create_hangouts_chat_integration( - active: true, - properties: params - ) - end - - it 'accepts branches_to_be_notified for update', :aggregate_failures do - put api("/projects/#{project.id}/services/#{integration_name}", user), params: params.merge(branches_to_be_notified: 'all') - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['properties']['branches_to_be_notified']).to eq('all') - end - - it 'only requires the webhook param' do - put api("/projects/#{project.id}/services/#{integration_name}", user), params: { webhook: 'https://hook.example.com' } - - expect(response).to have_gitlab_http_status(:ok) - end - end - - describe 'Pipelines Email Integration' do - let(:integration_name) { 'pipelines-email' } - - context 'notify_only_broken_pipelines property was saved as a string' do - before do - project.create_pipelines_email_integration( - active: false, - properties: { - "notify_only_broken_pipelines": "true", - "branches_to_be_notified": "default" - } - ) - end - - it 'returns boolean values for notify_only_broken_pipelines' do - get api("/projects/#{project.id}/services/#{integration_name}", user) - - expect(json_response['properties']['notify_only_broken_pipelines']).to eq(true) - end - end - end -end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index f5d261ba4c6..423e19c3971 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -48,6 +48,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do expect(json_response['admin_mode']).to be(false) expect(json_response['whats_new_variant']).to eq('all_tiers') expect(json_response['user_deactivation_emails_enabled']).to be(true) + expect(json_response['suggest_pipeline_enabled']).to be(true) end end @@ -135,7 +136,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do wiki_page_max_content_bytes: 12345, personal_access_token_prefix: "GL-", user_deactivation_emails_enabled: false, - admin_mode: true + admin_mode: true, + suggest_pipeline_enabled: false } expect(response).to have_gitlab_http_status(:ok) @@ -187,6 +189,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do expect(json_response['personal_access_token_prefix']).to eq("GL-") expect(json_response['admin_mode']).to be(true) expect(json_response['user_deactivation_emails_enabled']).to be(false) + expect(json_response['suggest_pipeline_enabled']).to be(false) end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index ee1911b0a26..fb01845b63a 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -1457,10 +1457,20 @@ RSpec.describe API::Users do describe "PUT /user/:id/credit_card_validation" do let(:credit_card_validated_time) { Time.utc(2020, 1, 1) } + let(:expiration_year) { Date.today.year + 10 } + let(:params) do + { + credit_card_validated_at: credit_card_validated_time, + credit_card_expiration_year: expiration_year, + credit_card_expiration_month: 1, + credit_card_holder_name: 'John Smith', + credit_card_mask_number: '1111' + } + end context 'when unauthenticated' do it 'returns authentication error' do - put api("/user/#{user.id}/credit_card_validation"), params: { credit_card_validated_at: credit_card_validated_time } + put api("/user/#{user.id}/credit_card_validation"), params: {} expect(response).to have_gitlab_http_status(:unauthorized) end @@ -1468,7 +1478,7 @@ RSpec.describe API::Users do context 'when authenticated as non-admin' do it "does not allow updating user's credit card validation", :aggregate_failures do - put api("/user/#{user.id}/credit_card_validation", user), params: { credit_card_validated_at: credit_card_validated_time } + put api("/user/#{user.id}/credit_card_validation", user), params: params expect(response).to have_gitlab_http_status(:forbidden) end @@ -1476,10 +1486,17 @@ RSpec.describe API::Users do context 'when authenticated as admin' do it "updates user's credit card validation", :aggregate_failures do - put api("/user/#{user.id}/credit_card_validation", admin), params: { credit_card_validated_at: credit_card_validated_time } + put api("/user/#{user.id}/credit_card_validation", admin), params: params + + user.reload expect(response).to have_gitlab_http_status(:ok) - expect(user.reload.credit_card_validated_at).to eq(credit_card_validated_time) + expect(user.credit_card_validation).to have_attributes( + credit_card_validated_at: credit_card_validated_time, + expiration_date: Date.new(expiration_year, 1, 31), + last_digits: 1111, + holder_name: 'John Smith' + ) end it "returns 400 error if credit_card_validated_at is missing" do @@ -1489,7 +1506,7 @@ RSpec.describe API::Users do end it 'returns 404 error if user not found' do - put api("/user/#{non_existing_record_id}/credit_card_validation", admin), params: { credit_card_validated_at: credit_card_validated_time } + put api("/user/#{non_existing_record_id}/credit_card_validation", admin), params: params expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') |