diff options
Diffstat (limited to 'spec/requests/api')
223 files changed, 11295 insertions, 4585 deletions
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb index 8c14ead9e42..45d1594c734 100644 --- a/spec/requests/api/access_requests_spec.rb +++ b/spec/requests/api/access_requests_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::AccessRequests, feature_category: :authentication_and_authorization do +RSpec.describe API::AccessRequests, feature_category: :system_access do let_it_be(:maintainer) { create(:user) } let_it_be(:developer) { create(:user) } let_it_be(:access_requester) { create(:user) } diff --git a/spec/requests/api/admin/batched_background_migrations_spec.rb b/spec/requests/api/admin/batched_background_migrations_spec.rb index d946ac17f3f..e88fba3fbe7 100644 --- a/spec/requests/api/admin/batched_background_migrations_spec.rb +++ b/spec/requests/api/admin/batched_background_migrations_spec.rb @@ -4,22 +4,23 @@ require 'spec_helper' RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :database do let(:admin) { create(:admin) } - let(:unauthorized_user) { create(:user) } describe 'GET /admin/batched_background_migrations/:id' do let!(:migration) { create(:batched_background_migration, :paused) } let(:database) { :main } let(:params) { { database: database } } + let(:path) { "/admin/batched_background_migrations/#{migration.id}" } + + it_behaves_like "GET request permissions for admin mode" subject(:show_migration) do - get api("/admin/batched_background_migrations/#{migration.id}", admin), params: { database: database } + get api(path, admin, admin_mode: true), params: { database: database } end it 'fetches the batched background migration' do show_migration aggregate_failures "testing response" do - expect(response).to have_gitlab_http_status(:ok) expect(json_response['id']).to eq(migration.id) expect(json_response['status']).to eq('paused') expect(json_response['job_class_name']).to eq(migration.job_class_name) @@ -29,7 +30,8 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab context 'when the batched background migration does not exist' do it 'returns 404' do - get api("/admin/batched_background_migrations/#{non_existing_record_id}", admin), params: params + get api("/admin/batched_background_migrations/#{non_existing_record_id}", admin, admin_mode: true), + params: params expect(response).to have_gitlab_http_status(:not_found) end @@ -50,19 +52,11 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab end end - context 'when authenticated as a non-admin user' do - it 'returns 403' do - get api("/admin/batched_background_migrations/#{migration.id}", unauthorized_user) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - context 'when the database name does not exist' do let(:database) { :wrong_database } - it 'returns bad request' do - get api("/admin/batched_background_migrations/#{migration.id}", admin), params: params + it 'returns bad request', :aggregate_failures do + get api(path, admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:bad_request) expect(response.body).to include('database does not have a valid value') @@ -72,13 +66,15 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab describe 'GET /admin/batched_background_migrations' do let!(:migration) { create(:batched_background_migration) } + let(:path) { '/admin/batched_background_migrations' } + + it_behaves_like "GET request permissions for admin mode" context 'when is an admin user' do it 'returns batched background migrations' do - get api('/admin/batched_background_migrations', admin) + get api(path, admin, admin_mode: true) aggregate_failures "testing response" do - expect(response).to have_gitlab_http_status(:ok) expect(json_response.count).to eq(1) expect(json_response.first['id']).to eq(migration.id) expect(json_response.first['job_class_name']).to eq(migration.job_class_name) @@ -105,14 +101,14 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield - get api('/admin/batched_background_migrations', admin), params: params + get api(path, admin, admin_mode: true), params: params end context 'when the database name does not exist' do let(:database) { :wrong_database } - it 'returns bad request' do - get api("/admin/batched_background_migrations", admin), params: params + it 'returns bad request', :aggregate_failures do + get api(path, admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:bad_request) expect(response.body).to include('database does not have a valid value') @@ -127,10 +123,9 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab create(:batched_background_migration, :active, gitlab_schema: schema) end - get api('/admin/batched_background_migrations', admin), params: params + get api(path, admin, admin_mode: true), params: params aggregate_failures "testing response" do - expect(response).to have_gitlab_http_status(:ok) expect(json_response.count).to eq(1) expect(json_response.first['id']).to eq(ci_database_migration.id) expect(json_response.first['job_class_name']).to eq(ci_database_migration.job_class_name) @@ -142,30 +137,24 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab end end end - - context 'when authenticated as a non-admin user' do - it 'returns 403' do - get api('/admin/batched_background_migrations', unauthorized_user) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end end describe 'PUT /admin/batched_background_migrations/:id/resume' do let!(:migration) { create(:batched_background_migration, :paused) } let(:database) { :main } let(:params) { { database: database } } + let(:path) { "/admin/batched_background_migrations/#{migration.id}/resume" } + + it_behaves_like "PUT request permissions for admin mode" subject(:resume) do - put api("/admin/batched_background_migrations/#{migration.id}/resume", admin), params: params + put api(path, admin, admin_mode: true), params: params end it 'pauses the batched background migration' do resume aggregate_failures "testing response" do - expect(response).to have_gitlab_http_status(:ok) expect(json_response['id']).to eq(migration.id) expect(json_response['status']).to eq('active') end @@ -173,7 +162,8 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab context 'when the batched background migration does not exist' do it 'returns 404' do - put api("/admin/batched_background_migrations/#{non_existing_record_id}/resume", admin), params: params + put api("/admin/batched_background_migrations/#{non_existing_record_id}/resume", admin, admin_mode: true), + params: params expect(response).to have_gitlab_http_status(:not_found) end @@ -183,7 +173,7 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab let!(:migration) { create(:batched_background_migration, :failed) } it 'returns 422' do - put api("/admin/batched_background_migrations/#{migration.id}/resume", admin), params: params + put api(path, admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:unprocessable_entity) end @@ -206,34 +196,28 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab context 'when the database name does not exist' do let(:database) { :wrong_database } - it 'returns bad request' do - put api("/admin/batched_background_migrations/#{migration.id}/resume", admin), params: params + it 'returns bad request', :aggregate_failures do + put api(path, admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:bad_request) expect(response.body).to include('database does not have a valid value') end end end - - context 'when authenticated as a non-admin user' do - it 'returns 403' do - put api("/admin/batched_background_migrations/#{migration.id}/resume", unauthorized_user) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end end describe 'PUT /admin/batched_background_migrations/:id/pause' do let!(:migration) { create(:batched_background_migration, :active) } let(:database) { :main } let(:params) { { database: database } } + let(:path) { "/admin/batched_background_migrations/#{migration.id}/pause" } + + it_behaves_like "PUT request permissions for admin mode" it 'pauses the batched background migration' do - put api("/admin/batched_background_migrations/#{migration.id}/pause", admin), params: params + put api(path, admin, admin_mode: true), params: params aggregate_failures "testing response" do - expect(response).to have_gitlab_http_status(:ok) expect(json_response['id']).to eq(migration.id) expect(json_response['status']).to eq('paused') end @@ -241,7 +225,8 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab context 'when the batched background migration does not exist' do it 'returns 404' do - put api("/admin/batched_background_migrations/#{non_existing_record_id}/pause", admin), params: params + put api("/admin/batched_background_migrations/#{non_existing_record_id}/pause", admin, admin_mode: true), + params: params expect(response).to have_gitlab_http_status(:not_found) end @@ -251,7 +236,7 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab let!(:migration) { create(:batched_background_migration, :failed) } it 'returns 422' do - put api("/admin/batched_background_migrations/#{migration.id}/pause", admin), params: params + put api(path, admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:unprocessable_entity) end @@ -268,27 +253,19 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab it 'uses the correct connection' do expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield - put api("/admin/batched_background_migrations/#{migration.id}/pause", admin), params: params + put api(path, admin, admin_mode: true), params: params end context 'when the database name does not exist' do let(:database) { :wrong_database } - it 'returns bad request' do - put api("/admin/batched_background_migrations/#{migration.id}/pause", admin), params: params + it 'returns bad request', :aggregate_failures do + put api(path, admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:bad_request) expect(response.body).to include('database does not have a valid value') end end end - - context 'when authenticated as a non-admin user' do - it 'returns 403' do - put api("/admin/batched_background_migrations/#{non_existing_record_id}/pause", unauthorized_user) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end end end diff --git a/spec/requests/api/admin/ci/variables_spec.rb b/spec/requests/api/admin/ci/variables_spec.rb index 4bdc44cb583..cd57cde74ff 100644 --- a/spec/requests/api/admin/ci/variables_spec.rb +++ b/spec/requests/api/admin/ci/variables_spec.rb @@ -2,71 +2,63 @@ require 'spec_helper' -RSpec.describe ::API::Admin::Ci::Variables do +RSpec.describe ::API::Admin::Ci::Variables, :aggregate_failures, feature_category: :pipeline_composition do let_it_be(:admin) { create(:admin) } let_it_be(:user) { create(:user) } + let_it_be(:variable) { create(:ci_instance_variable) } + let_it_be(:path) { '/admin/ci/variables' } describe 'GET /admin/ci/variables' do - let!(:variable) { create(:ci_instance_variable) } + it_behaves_like 'GET request permissions for admin mode' - it 'returns instance-level variables for admins', :aggregate_failures do - get api('/admin/ci/variables', admin) + it 'returns instance-level variables for admins' do + get api(path, admin, admin_mode: true) - expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_a(Array) end - it 'does not return instance-level variables for regular users' do - get api('/admin/ci/variables', user) - - expect(response).to have_gitlab_http_status(:forbidden) - end - it 'does not return instance-level variables for unauthorized users' do - get api('/admin/ci/variables') + get api(path, admin_mode: true) expect(response).to have_gitlab_http_status(:unauthorized) end end describe 'GET /admin/ci/variables/:key' do - let!(:variable) { create(:ci_instance_variable) } + let_it_be(:path) { "/admin/ci/variables/#{variable.key}" } + + it_behaves_like 'GET request permissions for admin mode' - it 'returns instance-level variable details for admins', :aggregate_failures do - get api("/admin/ci/variables/#{variable.key}", admin) + it 'returns instance-level variable details for admins' do + get api(path, admin, admin_mode: true) - expect(response).to have_gitlab_http_status(:ok) expect(json_response['value']).to eq(variable.value) expect(json_response['protected']).to eq(variable.protected?) expect(json_response['variable_type']).to eq(variable.variable_type) end it 'responds with 404 Not Found if requesting non-existing variable' do - get api('/admin/ci/variables/non_existing_variable', admin) + get api('/admin/ci/variables/non_existing_variable', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end - it 'does not return instance-level variable details for regular users' do - get api("/admin/ci/variables/#{variable.key}", user) - - expect(response).to have_gitlab_http_status(:forbidden) - end - it 'does not return instance-level variable details for unauthorized users' do - get api("/admin/ci/variables/#{variable.key}") + get api(path, admin_mode: true) expect(response).to have_gitlab_http_status(:unauthorized) end end describe 'POST /admin/ci/variables' do - context 'authorized user with proper permissions' do - let!(:variable) { create(:ci_instance_variable) } + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { { key: 'KEY', value: 'VALUE' } } + end - it 'creates variable for admins', :aggregate_failures do + context 'authorized user with proper permissions' do + it 'creates variable for admins' do expect do - post api('/admin/ci/variables', admin), + post api(path, admin, admin_mode: true), params: { key: 'TEST_VARIABLE_2', value: 'PROTECTED_VALUE_2', @@ -76,7 +68,6 @@ RSpec.describe ::API::Admin::Ci::Variables do } end.to change { ::Ci::InstanceVariable.count }.by(1) - expect(response).to have_gitlab_http_status(:created) expect(json_response['key']).to eq('TEST_VARIABLE_2') expect(json_response['value']).to eq('PROTECTED_VALUE_2') expect(json_response['protected']).to be_truthy @@ -90,13 +81,13 @@ RSpec.describe ::API::Admin::Ci::Variables do expect(::API::API::LOGGER).to receive(:info).with(include(params: include(masked_params))) - post api("/admin/ci/variables", user), + post api(path, user, admin_mode: true), params: { key: 'VAR_KEY', value: 'SENSITIVE', protected: true, masked: true } end - it 'creates variable with optional attributes', :aggregate_failures do + it 'creates variable with optional attributes' do expect do - post api('/admin/ci/variables', admin), + post api(path, admin, admin_mode: true), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', @@ -104,7 +95,6 @@ RSpec.describe ::API::Admin::Ci::Variables do } end.to change { ::Ci::InstanceVariable.count }.by(1) - expect(response).to have_gitlab_http_status(:created) expect(json_response['key']).to eq('TEST_VARIABLE_2') expect(json_response['value']).to eq('VALUE_2') expect(json_response['protected']).to be_falsey @@ -115,7 +105,7 @@ RSpec.describe ::API::Admin::Ci::Variables do it 'does not allow to duplicate variable key' do expect do - post api('/admin/ci/variables', admin), + post api(path, admin, admin_mode: true), params: { key: variable.key, value: 'VALUE_2' } end.not_to change { ::Ci::InstanceVariable.count } @@ -128,7 +118,7 @@ RSpec.describe ::API::Admin::Ci::Variables do MESSAGE expect do - post api('/admin/ci/variables', admin), + post api(path, admin, admin_mode: true), params: { key: 'too_long', value: SecureRandom.hex(10_001) } end.not_to change { ::Ci::InstanceVariable.count } @@ -138,17 +128,9 @@ RSpec.describe ::API::Admin::Ci::Variables do end end - context 'authorized user with invalid permissions' do - it 'does not create variable' do - post api('/admin/ci/variables', user) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - context 'unauthorized user' do it 'does not create variable' do - post api('/admin/ci/variables') + post api(path, admin_mode: true) expect(response).to have_gitlab_http_status(:unauthorized) end @@ -156,20 +138,23 @@ RSpec.describe ::API::Admin::Ci::Variables do end describe 'PUT /admin/ci/variables/:key' do - let!(:variable) { create(:ci_instance_variable) } + let_it_be(:path) { "/admin/ci/variables/#{variable.key}" } + let_it_be(:params) do + { + variable_type: 'file', + value: 'VALUE_1_UP', + protected: true, + masked: true, + raw: true + } + end + + it_behaves_like 'PUT request permissions for admin mode' context 'authorized user with proper permissions' do - it 'updates variable data', :aggregate_failures do - put api("/admin/ci/variables/#{variable.key}", admin), - params: { - variable_type: 'file', - value: 'VALUE_1_UP', - protected: true, - masked: true, - raw: true - } - - expect(response).to have_gitlab_http_status(:ok) + it 'updates variable data' do + put api(path, admin, admin_mode: true), params: params + expect(variable.reload.value).to eq('VALUE_1_UP') expect(variable.reload).to be_protected expect(json_response['variable_type']).to eq('file') @@ -182,28 +167,20 @@ RSpec.describe ::API::Admin::Ci::Variables do expect(::API::API::LOGGER).to receive(:info).with(include(params: include(masked_params))) - put api("/admin/ci/variables/#{variable.key}", admin), + put api(path, admin, admin_mode: true), params: { value: 'SENSITIVE', protected: true, masked: true } end it 'responds with 404 Not Found if requesting non-existing variable' do - put api('/admin/ci/variables/non_existing_variable', admin) + put api('/admin/ci/variables/non_existing_variable', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end end - context 'authorized user with invalid permissions' do - it 'does not update variable' do - put api("/admin/ci/variables/#{variable.key}", user) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - context 'unauthorized user' do it 'does not update variable' do - put api("/admin/ci/variables/#{variable.key}") + put api(path, admin_mode: true) expect(response).to have_gitlab_http_status(:unauthorized) end @@ -211,35 +188,27 @@ RSpec.describe ::API::Admin::Ci::Variables do end describe 'DELETE /admin/ci/variables/:key' do - let!(:variable) { create(:ci_instance_variable) } + let_it_be(:path) { "/admin/ci/variables/#{variable.key}" } + + it_behaves_like 'DELETE request permissions for admin mode' context 'authorized user with proper permissions' do it 'deletes variable' do expect do - delete api("/admin/ci/variables/#{variable.key}", admin) - - expect(response).to have_gitlab_http_status(:no_content) + delete api(path, admin, admin_mode: true) end.to change { ::Ci::InstanceVariable.count }.by(-1) end it 'responds with 404 Not Found if requesting non-existing variable' do - delete api('/admin/ci/variables/non_existing_variable', admin) + delete api('/admin/ci/variables/non_existing_variable', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end end - context 'authorized user with invalid permissions' do - it 'does not delete variable' do - delete api("/admin/ci/variables/#{variable.key}", user) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - context 'unauthorized user' do it 'does not delete variable' do - delete api("/admin/ci/variables/#{variable.key}") + delete api(path, admin_mode: true) expect(response).to have_gitlab_http_status(:unauthorized) end diff --git a/spec/requests/api/admin/instance_clusters_spec.rb b/spec/requests/api/admin/instance_clusters_spec.rb index 7b510f74fd4..f2e62533b78 100644 --- a/spec/requests/api/admin/instance_clusters_spec.rb +++ b/spec/requests/api/admin/instance_clusters_spec.rb @@ -2,10 +2,9 @@ require 'spec_helper' -RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_management do +RSpec.describe ::API::Admin::InstanceClusters, feature_category: :deployment_management do include KubernetesHelpers - let_it_be(:regular_user) { create(:user) } let_it_be(:admin_user) { create(:admin) } let_it_be(:project) { create(:project) } let_it_be(:project_cluster) do @@ -17,35 +16,27 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man let(:project_cluster_id) { project_cluster.id } describe "GET /admin/clusters" do + let_it_be(:path) { "/admin/clusters" } let_it_be(:clusters) do create_list(:cluster, 3, :provided_by_gcp, :instance, :production_environment) end - include_examples ':certificate_based_clusters feature flag API responses' do - let(:subject) { get api("/admin/clusters", admin_user) } - end + it_behaves_like 'GET request permissions for admin mode' - context "when authenticated as a non-admin user" do - it 'returns 403' do - get api('/admin/clusters', regular_user) - expect(response).to have_gitlab_http_status(:forbidden) - end + include_examples ':certificate_based_clusters feature flag API responses' do + let(:subject) { get api(path, admin_user, admin_mode: true) } end context "when authenticated as admin" do before do - get api("/admin/clusters", admin_user) - end - - it 'returns 200' do - expect(response).to have_gitlab_http_status(:ok) + get api(path, admin_user, admin_mode: true) end it 'includes pagination headers' do expect(response).to include_pagination_headers end - it 'only returns the instance clusters' do + it 'only returns the instance clusters', :aggregate_failures do cluster_ids = json_response.map { |cluster| cluster['id'] } expect(cluster_ids).to match_array(clusters.pluck(:id)) expect(cluster_ids).not_to include(project_cluster_id) @@ -60,19 +51,23 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man let_it_be(:cluster) do create(:cluster, :instance, :provided_by_gcp, :with_domain, - platform_kubernetes: platform_kubernetes, - user: admin_user) + { platform_kubernetes: platform_kubernetes, + user: admin_user }) end let(:cluster_id) { cluster.id } + let(:path) { "/admin/clusters/#{cluster_id}" } + + it_behaves_like 'GET request permissions for admin mode' + include_examples ':certificate_based_clusters feature flag API responses' do - let(:subject) { get api("/admin/clusters/#{cluster_id}", admin_user) } + let(:subject) { get api(path, admin_user, admin_mode: true) } end context "when authenticated as admin" do before do - get api("/admin/clusters/#{cluster_id}", admin_user) + get api(path, admin_user, admin_mode: true) end context "when no cluster associated to the ID" do @@ -84,15 +79,11 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man end context "when cluster with cluster_id exists" do - it 'returns 200' do - expect(response).to have_gitlab_http_status(:ok) - end - it 'returns the cluster with cluster_id' do expect(json_response['id']).to eq(cluster.id) end - it 'returns the cluster information' do + it 'returns the cluster information', :aggregate_failures do expect(json_response['provider_type']).to eq('gcp') expect(json_response['platform_type']).to eq('kubernetes') expect(json_response['environment_scope']).to eq('*') @@ -102,21 +93,21 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man expect(json_response['managed']).to be_truthy end - it 'returns kubernetes platform information' do + it 'returns kubernetes platform information', :aggregate_failures do platform = json_response['platform_kubernetes'] expect(platform['api_url']).to eq('https://kubernetes.example.com') expect(platform['ca_cert']).to be_present end - it 'returns user information' do + it 'returns user information', :aggregate_failures do user = json_response['user'] expect(user['id']).to eq(admin_user.id) expect(user['username']).to eq(admin_user.username) end - it 'returns GCP provider information' do + it 'returns GCP provider information', :aggregate_failures do gcp_provider = json_response['provider_gcp'] expect(gcp_provider['cluster_id']).to eq(cluster.id) @@ -140,18 +131,11 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man context 'when trying to get a project cluster via the instance cluster endpoint' do it 'returns 404' do - get api("/admin/clusters/#{project_cluster_id}", admin_user) + get api("/admin/clusters/#{project_cluster_id}", admin_user, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end end end - - context "when authenticated as a non-admin user" do - it 'returns 403' do - get api("/admin/clusters/#{cluster_id}", regular_user) - expect(response).to have_gitlab_http_status(:forbidden) - end - end end end @@ -159,6 +143,7 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man let(:api_url) { 'https://example.com' } let(:authorization_type) { 'rbac' } let(:clusterable) { Clusters::Instance.new } + let_it_be(:path) { '/admin/clusters/add' } let(:platform_kubernetes_attributes) do { @@ -196,20 +181,20 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man } end + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { cluster_params } + end + include_examples ':certificate_based_clusters feature flag API responses' do - let(:subject) { post api('/admin/clusters/add', admin_user), params: cluster_params } + let(:subject) { post api(path, admin_user, admin_mode: true), params: cluster_params } end context 'authorized user' do before do - post api('/admin/clusters/add', admin_user), params: cluster_params + post api(path, admin_user, admin_mode: true), params: cluster_params end context 'with valid params' do - it 'responds with 201' do - expect(response).to have_gitlab_http_status(:created) - end - it 'creates a new Clusters::Cluster', :aggregate_failures do cluster_result = Clusters::Cluster.find(json_response["id"]) platform_kubernetes = cluster_result.platform @@ -271,7 +256,7 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man context 'when an instance cluster already exists' do it 'allows user to add multiple clusters' do - post api('/admin/clusters/add', admin_user), params: multiple_cluster_params + post api(path, admin_user, admin_mode: true), params: multiple_cluster_params expect(Clusters::Instance.new.clusters.count).to eq(2) end @@ -280,8 +265,8 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man context 'with invalid params' do context 'when missing a required parameter' do - it 'responds with 400' do - post api('/admin/clusters/add', admin_user), params: invalid_cluster_params + it 'responds with 400', :aggregate_failures do + post api(path, admin_user, admin_mode: true), params: invalid_cluster_params expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eql('name is missing') end @@ -300,14 +285,6 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man end end end - - context 'non-authorized user' do - it 'responds with 403' do - post api('/admin/clusters/add', regular_user), params: cluster_params - - expect(response).to have_gitlab_http_status(:forbidden) - end - end end describe 'PUT /admin/clusters/:cluster_id' do @@ -329,23 +306,25 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man create(:cluster, :instance, :provided_by_gcp, domain: 'old-domain.com') end + let(:path) { "/admin/clusters/#{cluster.id}" } + + it_behaves_like 'PUT request permissions for admin mode' do + let(:params) { update_params } + end + include_examples ':certificate_based_clusters feature flag API responses' do - let(:subject) { put api("/admin/clusters/#{cluster.id}", admin_user), params: update_params } + let(:subject) { put api(path, admin_user, admin_mode: true), params: update_params } end context 'authorized user' do before do - put api("/admin/clusters/#{cluster.id}", admin_user), params: update_params + put api(path, admin_user, admin_mode: true), params: update_params cluster.reload end context 'with valid params' do - it 'responds with 200' do - expect(response).to have_gitlab_http_status(:ok) - end - - it 'updates cluster attributes' do + it 'updates cluster attributes', :aggregate_failures do expect(cluster.domain).to eq('new-domain.com') expect(cluster.managed).to be_falsy expect(cluster.enabled).to be_falsy @@ -359,7 +338,7 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man expect(response).to have_gitlab_http_status(:bad_request) end - it 'does not update cluster attributes' do + it 'does not update cluster attributes', :aggregate_failures do expect(cluster.domain).to eq('old-domain.com') expect(cluster.managed).to be_truthy expect(cluster.enabled).to be_truthy @@ -422,7 +401,7 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man expect(response).to have_gitlab_http_status(:ok) end - it 'updates platform kubernetes attributes' do + it 'updates platform kubernetes attributes', :aggregate_failures do platform_kubernetes = cluster.platform_kubernetes expect(cluster.name).to eq('new-name') @@ -435,26 +414,18 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man let(:cluster_id) { 1337 } it 'returns 404' do - put api("/admin/clusters/#{cluster_id}", admin_user), params: update_params + put api("/admin/clusters/#{cluster_id}", admin_user, admin_mode: true), params: update_params expect(response).to have_gitlab_http_status(:not_found) end end context 'when trying to update a project cluster via the instance cluster endpoint' do it 'returns 404' do - put api("/admin/clusters/#{project_cluster_id}", admin_user), params: update_params + put api("/admin/clusters/#{project_cluster_id}", admin_user, admin_mode: true), params: update_params expect(response).to have_gitlab_http_status(:not_found) end end end - - context 'non-authorized user' do - it 'responds with 403' do - put api("/admin/clusters/#{cluster.id}", regular_user), params: update_params - - expect(response).to have_gitlab_http_status(:forbidden) - end - end end describe 'DELETE /admin/clusters/:cluster_id' do @@ -464,17 +435,17 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man create(:cluster, :instance, :provided_by_gcp) end + let_it_be(:path) { "/admin/clusters/#{cluster.id}" } + + it_behaves_like 'DELETE request permissions for admin mode' + include_examples ':certificate_based_clusters feature flag API responses' do - let(:subject) { delete api("/admin/clusters/#{cluster.id}", admin_user), params: cluster_params } + let(:subject) { delete api(path, admin_user, admin_mode: true), params: cluster_params } end context 'authorized user' do before do - delete api("/admin/clusters/#{cluster.id}", admin_user), params: cluster_params - end - - it 'responds with 204' do - expect(response).to have_gitlab_http_status(:no_content) + delete api(path, admin_user, admin_mode: true), params: cluster_params end it 'deletes the cluster' do @@ -485,25 +456,17 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man let(:cluster_id) { 1337 } it 'returns 404' do - delete api("/admin/clusters/#{cluster_id}", admin_user) + delete api(path, admin_user, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end end context 'when trying to update a project cluster via the instance cluster endpoint' do it 'returns 404' do - delete api("/admin/clusters/#{project_cluster_id}", admin_user) + delete api("/admin/clusters/#{project_cluster_id}", admin_user, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end end end - - context 'non-authorized user' do - it 'responds with 403' do - delete api("/admin/clusters/#{cluster.id}", regular_user), params: cluster_params - - expect(response).to have_gitlab_http_status(:forbidden) - end - end end end diff --git a/spec/requests/api/admin/plan_limits_spec.rb b/spec/requests/api/admin/plan_limits_spec.rb index 2de7a66d803..6085b48c7c2 100644 --- a/spec/requests/api/admin/plan_limits_spec.rb +++ b/spec/requests/api/admin/plan_limits_spec.rb @@ -2,30 +2,22 @@ require 'spec_helper' -RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owned do - let_it_be(:user) { create(:user) } +RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :shared do let_it_be(:admin) { create(:admin) } let_it_be(:plan) { create(:plan, name: 'default') } + let_it_be(:path) { '/application/plan_limits' } describe 'GET /application/plan_limits' do - context 'as a non-admin user' do - it 'returns 403' do - get api('/application/plan_limits', user) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end + it_behaves_like 'GET request permissions for admin mode' context 'as an admin user' do context 'no params' do - it 'returns plan limits' do - get api('/application/plan_limits', admin) + it 'returns plan limits', :aggregate_failures do + get api(path, admin, admin_mode: true) - expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Hash expect(json_response['ci_pipeline_size']).to eq(Plan.default.actual_limits.ci_pipeline_size) expect(json_response['ci_active_jobs']).to eq(Plan.default.actual_limits.ci_active_jobs) - expect(json_response['ci_active_pipelines']).to eq(Plan.default.actual_limits.ci_active_pipelines) expect(json_response['ci_project_subscriptions']).to eq(Plan.default.actual_limits.ci_project_subscriptions) expect(json_response['ci_pipeline_schedules']).to eq(Plan.default.actual_limits.ci_pipeline_schedules) expect(json_response['ci_needs_size_limit']).to eq(Plan.default.actual_limits.ci_needs_size_limit) @@ -49,14 +41,13 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne @params = { plan_name: 'default' } end - it 'returns plan limits' do - get api('/application/plan_limits', admin), params: @params + it 'returns plan limits', :aggregate_failures do + get api(path, admin, admin_mode: true), params: @params expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Hash expect(json_response['ci_pipeline_size']).to eq(Plan.default.actual_limits.ci_pipeline_size) expect(json_response['ci_active_jobs']).to eq(Plan.default.actual_limits.ci_active_jobs) - expect(json_response['ci_active_pipelines']).to eq(Plan.default.actual_limits.ci_active_pipelines) expect(json_response['ci_project_subscriptions']).to eq(Plan.default.actual_limits.ci_project_subscriptions) expect(json_response['ci_pipeline_schedules']).to eq(Plan.default.actual_limits.ci_pipeline_schedules) expect(json_response['ci_needs_size_limit']).to eq(Plan.default.actual_limits.ci_needs_size_limit) @@ -80,8 +71,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne @params = { plan_name: 'my-plan' } end - it 'returns validation error' do - get api('/application/plan_limits', admin), params: @params + it 'returns validation error', :aggregate_failures do + get api(path, admin, admin_mode: true), params: @params expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq('plan_name does not have a valid value') @@ -91,22 +82,17 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne end describe 'PUT /application/plan_limits' do - context 'as a non-admin user' do - it 'returns 403' do - put api('/application/plan_limits', user), params: { plan_name: 'default' } - - expect(response).to have_gitlab_http_status(:forbidden) - end + it_behaves_like 'PUT request permissions for admin mode' do + let(:params) { { 'plan_name': 'default' } } end context 'as an admin user' do context 'correct params' do - it 'updates multiple plan limits' do - put api('/application/plan_limits', admin), params: { + it 'updates multiple plan limits', :aggregate_failures do + put api(path, admin, admin_mode: true), params: { 'plan_name': 'default', 'ci_pipeline_size': 101, 'ci_active_jobs': 102, - 'ci_active_pipelines': 103, 'ci_project_subscriptions': 104, 'ci_pipeline_schedules': 105, 'ci_needs_size_limit': 106, @@ -124,11 +110,9 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne 'pipeline_hierarchy_size': 250 } - expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Hash expect(json_response['ci_pipeline_size']).to eq(101) expect(json_response['ci_active_jobs']).to eq(102) - expect(json_response['ci_active_pipelines']).to eq(103) expect(json_response['ci_project_subscriptions']).to eq(104) expect(json_response['ci_pipeline_schedules']).to eq(105) expect(json_response['ci_needs_size_limit']).to eq(106) @@ -146,8 +130,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne expect(json_response['pipeline_hierarchy_size']).to eq(250) end - it 'updates single plan limits' do - put api('/application/plan_limits', admin), params: { + it 'updates single plan limits', :aggregate_failures do + put api(path, admin, admin_mode: true), params: { 'plan_name': 'default', 'maven_max_file_size': 100 } @@ -159,8 +143,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne end context 'empty params' do - it 'fails to update plan limits' do - put api('/application/plan_limits', admin), params: {} + it 'fails to update plan limits', :aggregate_failures do + put api(path, admin, admin_mode: true), params: {} expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to match('plan_name is missing') @@ -168,12 +152,11 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne end context 'params with wrong type' do - it 'fails to update plan limits' do - put api('/application/plan_limits', admin), params: { + it 'fails to update plan limits', :aggregate_failures do + put api(path, admin, admin_mode: true), params: { 'plan_name': 'default', 'ci_pipeline_size': 'z', 'ci_active_jobs': 'y', - 'ci_active_pipelines': 'x', 'ci_project_subscriptions': 'w', 'ci_pipeline_schedules': 'v', 'ci_needs_size_limit': 'u', @@ -195,7 +178,6 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne expect(json_response['error']).to include( 'ci_pipeline_size is invalid', 'ci_active_jobs is invalid', - 'ci_active_pipelines is invalid', 'ci_project_subscriptions is invalid', 'ci_pipeline_schedules is invalid', 'ci_needs_size_limit is invalid', @@ -216,8 +198,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne end context 'missing plan_name in params' do - it 'fails to update plan limits' do - put api('/application/plan_limits', admin), params: { 'conan_max_file_size': 0 } + it 'fails to update plan limits', :aggregate_failures do + put api(path, admin, admin_mode: true), params: { 'conan_max_file_size': 0 } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to match('plan_name is missing') @@ -229,8 +211,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne Plan.default.actual_limits.update!({ 'golang_max_file_size': 1000 }) end - it 'updates only declared plan limits' do - put api('/application/plan_limits', admin), params: { + it 'updates only declared plan limits', :aggregate_failures do + put api(path, admin, admin_mode: true), params: { 'plan_name': 'default', 'pypi_max_file_size': 200, 'golang_max_file_size': 999 diff --git a/spec/requests/api/admin/sidekiq_spec.rb b/spec/requests/api/admin/sidekiq_spec.rb index 0b456721d4f..eca12c8e433 100644 --- a/spec/requests/api/admin/sidekiq_spec.rb +++ b/spec/requests/api/admin/sidekiq_spec.rb @@ -2,18 +2,10 @@ require 'spec_helper' -RSpec.describe API::Admin::Sidekiq, :clean_gitlab_redis_queues, feature_category: :not_owned do +RSpec.describe API::Admin::Sidekiq, :clean_gitlab_redis_queues, feature_category: :shared do let_it_be(:admin) { create(:admin) } describe 'DELETE /admin/sidekiq/queues/:queue_name' do - context 'when the user is not an admin' do - it 'returns a 403' do - delete api("/admin/sidekiq/queues/authorized_projects?user=#{admin.username}", create(:user)) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - context 'when the user is an admin' do around do |example| Sidekiq::Queue.new('authorized_projects').clear @@ -31,14 +23,21 @@ RSpec.describe API::Admin::Sidekiq, :clean_gitlab_redis_queues, feature_category end context 'valid request' do - it 'returns info about the deleted jobs' do + before do add_job(admin, [1]) add_job(admin, [2]) add_job(create(:user), [3]) + end + + let_it_be(:path) { "/admin/sidekiq/queues/authorized_projects?user=#{admin.username}&worker_class=AuthorizedProjectsWorker" } - delete api("/admin/sidekiq/queues/authorized_projects?user=#{admin.username}&worker_class=AuthorizedProjectsWorker", admin) + it_behaves_like 'DELETE request permissions for admin mode' do + let(:success_status_code) { :ok } + end + + it 'returns info about the deleted jobs' do + delete api(path, admin, admin_mode: true) - expect(response).to have_gitlab_http_status(:ok) expect(json_response).to eq('completed' => true, 'deleted_jobs' => 2, 'queue_size' => 1) @@ -47,7 +46,7 @@ RSpec.describe API::Admin::Sidekiq, :clean_gitlab_redis_queues, feature_category context 'when no required params are provided' do it 'returns a 400' do - delete api("/admin/sidekiq/queues/authorized_projects?user_2=#{admin.username}", admin) + delete api("/admin/sidekiq/queues/authorized_projects?user_2=#{admin.username}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) end @@ -55,7 +54,7 @@ RSpec.describe API::Admin::Sidekiq, :clean_gitlab_redis_queues, feature_category context 'when the queue does not exist' do it 'returns a 404' do - delete api("/admin/sidekiq/queues/authorized_projects_2?user=#{admin.username}", admin) + delete api("/admin/sidekiq/queues/authorized_projects_2?user=#{admin.username}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end diff --git a/spec/requests/api/api_guard/admin_mode_middleware_spec.rb b/spec/requests/api/api_guard/admin_mode_middleware_spec.rb index 21f3691c20b..7268fa2c90b 100644 --- a/spec/requests/api/api_guard/admin_mode_middleware_spec.rb +++ b/spec/requests/api/api_guard/admin_mode_middleware_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::APIGuard::AdminModeMiddleware, :request_store, feature_category: :not_owned do +RSpec.describe API::APIGuard::AdminModeMiddleware, :request_store, feature_category: :shared do let(:user) { create(:admin) } it 'is loaded' do diff --git a/spec/requests/api/api_guard/response_coercer_middleware_spec.rb b/spec/requests/api/api_guard/response_coercer_middleware_spec.rb index 77498c2e2b3..4a993d0b255 100644 --- a/spec/requests/api/api_guard/response_coercer_middleware_spec.rb +++ b/spec/requests/api/api_guard/response_coercer_middleware_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::APIGuard::ResponseCoercerMiddleware, feature_category: :not_owned do +RSpec.describe API::APIGuard::ResponseCoercerMiddleware, feature_category: :shared do using RSpec::Parameterized::TableSyntax it 'is loaded' do diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb index 35851fff6c8..219c7dbdbc5 100644 --- a/spec/requests/api/api_spec.rb +++ b/spec/requests/api/api_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::API, feature_category: :authentication_and_authorization do +RSpec.describe API::API, feature_category: :system_access do include GroupAPIHelpers describe 'Record user last activity in after hook' do @@ -359,4 +359,26 @@ RSpec.describe API::API, feature_category: :authentication_and_authorization do end end end + + describe 'Handle Gitlab::Git::ResourceExhaustedError exception' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository, creator: user) } + + before do + project.add_maintainer(user) + allow(Gitlab::GitalyClient).to receive(:call).with(any_args).and_raise( + Gitlab::Git::ResourceExhaustedError.new("Upstream Gitaly has been exhausted. Try again later", 50) + ) + end + + it 'returns 429 status with exhausted' do + get api("/projects/#{project.id}/repository/commits", user) + + expect(response).to have_gitlab_http_status(:too_many_requests) + expect(response.headers['Retry-After']).to be(50) + expect(json_response).to eql( + 'message' => 'Upstream Gitaly has been exhausted. Try again later' + ) + end + end end diff --git a/spec/requests/api/appearance_spec.rb b/spec/requests/api/appearance_spec.rb index c08ecae28e8..2ea4dcce7d8 100644 --- a/spec/requests/api/appearance_spec.rb +++ b/spec/requests/api/appearance_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do +RSpec.describe API::Appearance, 'Appearance', :aggregate_failures, feature_category: :navigation do let_it_be(:user) { create(:user) } let_it_be(:admin) { create(:admin) } let_it_be(:path) { "/application/appearance" } @@ -12,7 +12,7 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do context 'as an admin user' do it "returns appearance" do - get api("/application/appearance", admin, admin_mode: true) + get api(path, admin, admin_mode: true) expect(json_response).to be_an Hash expect(json_response['description']).to eq('') @@ -36,12 +36,14 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do end describe "PUT /application/appearance" do - it_behaves_like 'PUT request permissions for admin mode', { title: "Test" } + it_behaves_like 'PUT request permissions for admin mode' do + let(:params) { { title: "Test" } } + end context 'as an admin user' do context "instance basics" do it "allows updating the settings" do - put api("/application/appearance", admin, admin_mode: true), params: { + put api(path, admin, admin_mode: true), params: { title: "GitLab Test Instance", description: "gitlab-test.example.com", pwa_name: "GitLab PWA Test", @@ -81,7 +83,7 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do email_header_and_footer_enabled: true } - put api("/application/appearance", admin, admin_mode: true), params: settings + put api(path, admin, admin_mode: true), params: settings expect(response).to have_gitlab_http_status(:ok) settings.each do |attribute, value| @@ -91,14 +93,14 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do context "fails on invalid color values" do it "with message_font_color" do - put api("/application/appearance", admin, admin_mode: true), params: { message_font_color: "No Color" } + put api(path, admin, admin_mode: true), params: { message_font_color: "No Color" } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']['message_font_color']).to contain_exactly('must be a valid color code') end it "with message_background_color" do - put api("/application/appearance", admin, admin_mode: true), params: { message_background_color: "#1" } + put api(path, admin, admin_mode: true), params: { message_background_color: "#1" } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']['message_background_color']).to contain_exactly('must be a valid color code') @@ -110,7 +112,7 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do let_it_be(:appearance) { create(:appearance) } it "allows updating the image files" do - put api("/application/appearance", admin, admin_mode: true), params: { + put api(path, admin, admin_mode: true), params: { logo: fixture_file_upload("spec/fixtures/dk.png", "image/png"), header_logo: fixture_file_upload("spec/fixtures/dk.png", "image/png"), pwa_icon: fixture_file_upload("spec/fixtures/dk.png", "image/png"), @@ -126,14 +128,14 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do context "fails on invalid color images" do it "with string instead of file" do - put api("/application/appearance", admin, admin_mode: true), params: { logo: 'not-a-file.png' } + put api(path, admin, admin_mode: true), params: { logo: 'not-a-file.png' } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq("logo is invalid") end it "with .svg file instead of .png" do - put api("/application/appearance", admin, admin_mode: true), params: { favicon: fixture_file_upload("spec/fixtures/logo_sample.svg", "image/svg") } + put api(path, admin, admin_mode: true), params: { favicon: fixture_file_upload("spec/fixtures/logo_sample.svg", "image/svg") } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']['favicon']).to contain_exactly("You are not allowed to upload \"svg\" files, allowed types: png, ico") diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb index b81cdcfea8e..16e24807e67 100644 --- a/spec/requests/api/applications_spec.rb +++ b/spec/requests/api/applications_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Applications, :api, feature_category: :authentication_and_authorization do +RSpec.describe API::Applications, :aggregate_failures, :api, feature_category: :system_access do let_it_be(:admin) { create(:admin) } let_it_be(:user) { create(:user) } let_it_be(:scopes) { 'api' } @@ -10,7 +10,9 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au let!(:application) { create(:application, name: 'another_application', owner: nil, redirect_uri: 'http://other_application.url', scopes: scopes) } describe 'POST /applications' do - it_behaves_like 'POST request permissions for admin mode', { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'api' } + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'api' } } + end context 'authenticated and authorized user' do it 'creates and returns an OAuth application' do @@ -22,7 +24,7 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au expect(json_response).to be_a Hash expect(json_response['application_id']).to eq application.uid - expect(json_response['secret']).to eq application.secret + expect(application.secret_matches?(json_response['secret'])).to eq(true) expect(json_response['callback_url']).to eq application.redirect_uri expect(json_response['confidential']).to eq application.confidential expect(application.scopes.to_s).to eq('api') @@ -133,7 +135,7 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au context 'authorized user without authorization' do it 'does not create application' do expect do - post api('/applications', user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: scopes } + post api(path, user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: scopes } end.not_to change { Doorkeeper::Application.count } end end diff --git a/spec/requests/api/avatar_spec.rb b/spec/requests/api/avatar_spec.rb index fcef5b6ca78..0a77b6e228e 100644 --- a/spec/requests/api/avatar_spec.rb +++ b/spec/requests/api/avatar_spec.rb @@ -19,6 +19,7 @@ RSpec.describe API::Avatar, feature_category: :user_profile do expect(response).to have_gitlab_http_status(:ok) expect(json_response['avatar_url']).to eql("#{::Settings.gitlab.base_url}#{user.avatar.local_url}") + is_expected.to have_request_urgency(:medium) end end diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index 87dc06b7d15..22c67a253e3 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::AwardEmoji, feature_category: :not_owned do +RSpec.describe API::AwardEmoji, feature_category: :shared do let_it_be_with_reload(:project) { create(:project, :private) } let_it_be(:user) { create(:user) } let_it_be(:issue) { create(:issue, project: project) } diff --git a/spec/requests/api/badges_spec.rb b/spec/requests/api/badges_spec.rb index 6c6a7cc7cc6..1c09c1129a2 100644 --- a/spec/requests/api/badges_spec.rb +++ b/spec/requests/api/badges_spec.rb @@ -72,9 +72,9 @@ RSpec.describe API::Badges, feature_category: :projects do context 'when authenticated as a non-member' do %i[maintainer developer access_requester stranger].each do |type| - let(:badge) { source.badges.first } - context "as a #{type}" do + let(:badge) { source.badges.first } + it 'returns 200', :quarantine do user = public_send(type) diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb index 5cbb7dbfa12..530c81364a8 100644 --- a/spec/requests/api/broadcast_messages_spec.rb +++ b/spec/requests/api/broadcast_messages_spec.rb @@ -2,16 +2,16 @@ require 'spec_helper' -RSpec.describe API::BroadcastMessages, feature_category: :onboarding do - let_it_be(:user) { create(:user) } +RSpec.describe API::BroadcastMessages, :aggregate_failures, feature_category: :onboarding do let_it_be(:admin) { create(:admin) } let_it_be(:message) { create(:broadcast_message) } + let_it_be(:path) { '/broadcast_messages' } describe 'GET /broadcast_messages' do it 'returns an Array of BroadcastMessages' do create(:broadcast_message) - get api('/broadcast_messages') + get api(path) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -22,8 +22,10 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do end describe 'GET /broadcast_messages/:id' do + let_it_be(:path) { "#{path}/#{message.id}" } + it 'returns the specified message' do - get api("/broadcast_messages/#{message.id}") + get api(path) expect(response).to have_gitlab_http_status(:ok) expect(json_response['id']).to eq message.id @@ -33,16 +35,14 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do end describe 'POST /broadcast_messages' do - it 'returns a 401 for anonymous users' do - post api('/broadcast_messages'), params: attributes_for(:broadcast_message) - - expect(response).to have_gitlab_http_status(:unauthorized) + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { { message: 'Test message' } } end - it 'returns a 403 for users' do - post api('/broadcast_messages', user), params: attributes_for(:broadcast_message) + it 'returns a 401 for anonymous users' do + post api(path), params: attributes_for(:broadcast_message) - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:unauthorized) end context 'as an admin' do @@ -50,7 +50,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do attrs = attributes_for(:broadcast_message) attrs.delete(:message) - post api('/broadcast_messages', admin), params: attrs + post api(path, admin, admin_mode: true), params: attrs expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq 'message is missing' @@ -59,7 +59,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do it 'defines sane default start and end times' do time = Time.zone.parse('2016-07-02 10:11:12') travel_to(time) do - post api('/broadcast_messages', admin), params: { message: 'Test message' } + post api(path, admin, admin_mode: true), params: { message: 'Test message' } expect(response).to have_gitlab_http_status(:created) expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z' @@ -70,7 +70,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do it 'accepts a custom background and foreground color' do attrs = attributes_for(:broadcast_message, color: '#000000', font: '#cecece') - post api('/broadcast_messages', admin), params: attrs + post api(path, admin, admin_mode: true), params: attrs expect(response).to have_gitlab_http_status(:created) expect(json_response['color']).to eq attrs[:color] @@ -81,7 +81,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do target_access_levels = [Gitlab::Access::GUEST, Gitlab::Access::DEVELOPER] attrs = attributes_for(:broadcast_message, target_access_levels: target_access_levels) - post api('/broadcast_messages', admin), params: attrs + post api(path, admin, admin_mode: true), params: attrs expect(response).to have_gitlab_http_status(:created) expect(json_response['target_access_levels']).to eq attrs[:target_access_levels] @@ -90,7 +90,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do it 'accepts a target path' do attrs = attributes_for(:broadcast_message, target_path: "*/welcome") - post api('/broadcast_messages', admin), params: attrs + post api(path, admin, admin_mode: true), params: attrs expect(response).to have_gitlab_http_status(:created) expect(json_response['target_path']).to eq attrs[:target_path] @@ -99,7 +99,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do it 'accepts a broadcast type' do attrs = attributes_for(:broadcast_message, broadcast_type: 'notification') - post api('/broadcast_messages', admin), params: attrs + post api(path, admin, admin_mode: true), params: attrs expect(response).to have_gitlab_http_status(:created) expect(json_response['broadcast_type']).to eq attrs[:broadcast_type] @@ -108,7 +108,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do it 'uses default broadcast type' do attrs = attributes_for(:broadcast_message) - post api('/broadcast_messages', admin), params: attrs + post api(path, admin, admin_mode: true), params: attrs expect(response).to have_gitlab_http_status(:created) expect(json_response['broadcast_type']).to eq 'banner' @@ -117,7 +117,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do it 'errors for invalid broadcast type' do attrs = attributes_for(:broadcast_message, broadcast_type: 'invalid-type') - post api('/broadcast_messages', admin), params: attrs + post api(path, admin, admin_mode: true), params: attrs expect(response).to have_gitlab_http_status(:bad_request) end @@ -125,7 +125,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do it 'accepts an active dismissable value' do attrs = { message: 'new message', dismissable: true } - post api('/broadcast_messages', admin), params: attrs + post api(path, admin, admin_mode: true), params: attrs expect(response).to have_gitlab_http_status(:created) expect(json_response['dismissable']).to eq true @@ -134,27 +134,25 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do end describe 'PUT /broadcast_messages/:id' do - it 'returns a 401 for anonymous users' do - put api("/broadcast_messages/#{message.id}"), - params: attributes_for(:broadcast_message) + let_it_be(:path) { "#{path}/#{message.id}" } - expect(response).to have_gitlab_http_status(:unauthorized) + it_behaves_like 'PUT request permissions for admin mode' do + let(:params) { { message: 'Test message' } } end - it 'returns a 403 for users' do - put api("/broadcast_messages/#{message.id}", user), + it 'returns a 401 for anonymous users' do + put api(path), params: attributes_for(:broadcast_message) - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:unauthorized) end context 'as an admin' do it 'accepts new background and foreground colors' do attrs = { color: '#000000', font: '#cecece' } - put api("/broadcast_messages/#{message.id}", admin), params: attrs + put api(path, admin, admin_mode: true), params: attrs - expect(response).to have_gitlab_http_status(:ok) expect(json_response['color']).to eq attrs[:color] expect(json_response['font']).to eq attrs[:font] end @@ -164,7 +162,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do travel_to(time) do attrs = { starts_at: Time.zone.now, ends_at: 3.hours.from_now } - put api("/broadcast_messages/#{message.id}", admin), params: attrs + put api(path, admin, admin_mode: true), params: attrs expect(response).to have_gitlab_http_status(:ok) expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z' @@ -175,7 +173,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do it 'accepts a new message' do attrs = { message: 'new message' } - put api("/broadcast_messages/#{message.id}", admin), params: attrs + put api(path, admin, admin_mode: true), params: attrs expect(response).to have_gitlab_http_status(:ok) expect { message.reload }.to change { message.message }.to('new message') @@ -184,7 +182,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do it 'accepts a new target_access_levels' do attrs = { target_access_levels: [Gitlab::Access::MAINTAINER] } - put api("/broadcast_messages/#{message.id}", admin), params: attrs + put api(path, admin, admin_mode: true), params: attrs expect(response).to have_gitlab_http_status(:ok) expect(json_response['target_access_levels']).to eq attrs[:target_access_levels] @@ -193,7 +191,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do it 'accepts a new target_path' do attrs = { target_path: '*/welcome' } - put api("/broadcast_messages/#{message.id}", admin), params: attrs + put api(path, admin, admin_mode: true), params: attrs expect(response).to have_gitlab_http_status(:ok) expect(json_response['target_path']).to eq attrs[:target_path] @@ -202,7 +200,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do it 'accepts a new broadcast_type' do attrs = { broadcast_type: 'notification' } - put api("/broadcast_messages/#{message.id}", admin), params: attrs + put api(path, admin, admin_mode: true), params: attrs expect(response).to have_gitlab_http_status(:ok) expect(json_response['broadcast_type']).to eq attrs[:broadcast_type] @@ -211,7 +209,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do it 'errors for invalid broadcast type' do attrs = { broadcast_type: 'invalid-type' } - put api("/broadcast_messages/#{message.id}", admin), params: attrs + put api(path, admin, admin_mode: true), params: attrs expect(response).to have_gitlab_http_status(:bad_request) end @@ -219,7 +217,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do it 'accepts a new dismissable value' do attrs = { message: 'new message', dismissable: true } - put api("/broadcast_messages/#{message.id}", admin), params: attrs + put api(path, admin, admin_mode: true), params: attrs expect(response).to have_gitlab_http_status(:ok) expect(json_response['dismissable']).to eq true @@ -228,27 +226,24 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do end describe 'DELETE /broadcast_messages/:id' do - it 'returns a 401 for anonymous users' do - delete api("/broadcast_messages/#{message.id}"), - params: attributes_for(:broadcast_message) + let_it_be(:path) { "#{path}/#{message.id}" } - expect(response).to have_gitlab_http_status(:unauthorized) - end + it_behaves_like 'DELETE request permissions for admin mode' - it 'returns a 403 for users' do - delete api("/broadcast_messages/#{message.id}", user), + it 'returns a 401 for anonymous users' do + delete api(path), params: attributes_for(:broadcast_message) - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:unauthorized) end it_behaves_like '412 response' do - let(:request) { api("/broadcast_messages/#{message.id}", admin) } + let(:request) { api("/broadcast_messages/#{message.id}", admin, admin_mode: true) } end it 'deletes the broadcast message for admins' do expect do - delete api("/broadcast_messages/#{message.id}", admin) + delete api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:no_content) end.to change { BroadcastMessage.count }.by(-1) diff --git a/spec/requests/api/bulk_imports_spec.rb b/spec/requests/api/bulk_imports_spec.rb index 23dfe865ba3..b159d4ad445 100644 --- a/spec/requests/api/bulk_imports_spec.rb +++ b/spec/requests/api/bulk_imports_spec.rb @@ -75,6 +75,8 @@ RSpec.describe API::BulkImports, feature_category: :importers do end describe 'POST /bulk_imports' do + let_it_be(:destination_namespace) { create(:group) } + let(:request) { post api('/bulk_imports', user), params: params } let(:destination_param) { { destination_slug: 'destination_slug' } } let(:params) do @@ -87,12 +89,15 @@ RSpec.describe API::BulkImports, feature_category: :importers do { source_type: 'group_entity', source_full_path: 'full_path', - destination_namespace: 'destination_namespace' + destination_namespace: destination_namespace.path }.merge(destination_param) ] } end + let(:source_entity_type) { BulkImports::CreateService::ENTITY_TYPES_MAPPING.fetch(params[:entities][0][:source_type]) } + let(:source_entity_identifier) { ERB::Util.url_encode(params[:entities][0][:source_full_path]) } + before do allow_next_instance_of(BulkImports::Clients::HTTP) do |instance| allow(instance) @@ -103,6 +108,10 @@ RSpec.describe API::BulkImports, feature_category: :importers do .to receive(:instance_enterprise) .and_return(false) end + stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=access_token") + .to_return(status: 200, body: "", headers: {}) + + destination_namespace.add_owner(user) end shared_examples 'starting a new migration' do @@ -192,7 +201,7 @@ RSpec.describe API::BulkImports, feature_category: :importers do { source_type: 'group_entity', source_full_path: 'full_path', - destination_namespace: 'destination_namespace' + destination_namespace: destination_namespace.path } ] } @@ -214,20 +223,17 @@ RSpec.describe API::BulkImports, feature_category: :importers do request expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq("entities[0][source_full_path] must be a relative path and not include protocol, sub-domain, " \ - "or domain information. E.g. 'source/full/path' not 'https://example.com/source/full/path'") + "or domain information. For example, 'source/full/path' not 'https://example.com/source/full/path'") end end - context 'when the destination_namespace is invalid' do + context 'when the destination_namespace does not exist' do it 'returns invalid error' do - params[:entities][0][:destination_namespace] = "?not a destination-namespace" + params[:entities][0][:destination_namespace] = "invalid-destination-namespace" request - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']).to eq("entities[0][destination_namespace] cannot start with a dash or forward slash, " \ - "or end with a period or forward slash. It can only contain alphanumeric " \ - "characters, periods, underscores, forward slashes and dashes. " \ - "E.g. 'destination_namespace' or 'destination/namespace'") + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response['message']).to eq("Import failed. Destination 'invalid-destination-namespace' is invalid, or you don't have permission.") end end @@ -243,15 +249,35 @@ RSpec.describe API::BulkImports, feature_category: :importers do end context 'when the destination_slug is invalid' do - it 'returns invalid error' do + it 'returns invalid error when restricting special characters is disabled' do + Feature.disable(:restrict_special_characters_in_namespace_path) + + params[:entities][0][:destination_slug] = 'des?tin?atoi-slugg' + + request + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to include("entities[0][destination_slug] cannot start with " \ + "a non-alphanumeric character except for periods or " \ + "underscores, can contain only alphanumeric characters, " \ + "periods, and underscores, cannot end with a period or " \ + "forward slash, and has no leading or trailing forward " \ + "slashes. It can only contain alphanumeric characters, " \ + "periods, underscores, and dashes. For example, " \ + "'destination_namespace' not 'destination/namespace'") + end + + it 'returns invalid error when restricting special characters is enabled' do + Feature.enable(:restrict_special_characters_in_namespace_path) + params[:entities][0][:destination_slug] = 'des?tin?atoi-slugg' request expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']).to include("entities[0][destination_slug] cannot start with a dash " \ - "or forward slash, or end with a period or forward slash. " \ - "It can only contain alphanumeric characters, periods, underscores, and dashes. " \ - "E.g. 'destination_namespace' not 'destination/namespace'") + expect(json_response['error']).to include("entities[0][destination_slug] must not start or " \ + "end with a special character and must not contain " \ + "consecutive special characters. It can only contain " \ + "alphanumeric characters, periods, underscores, and " \ + "dashes. For example, 'destination_namespace' not 'destination/namespace'") end end @@ -271,12 +297,41 @@ RSpec.describe API::BulkImports, feature_category: :importers do } end + it 'returns blocked url message in the error' do + request + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + + expect(json_response['message']).to include("Url is blocked: Only allowed schemes are http, https") + end + end + + context 'when source instance setting is disabled' do + let(:params) do + { + configuration: { + url: 'http://gitlab.example', + access_token: 'access_token' + }, + entities: [ + source_type: 'group_entity', + source_full_path: 'full_path', + destination_slug: 'destination_slug', + destination_namespace: 'destination_namespace' + ] + } + end + it 'returns blocked url error' do + stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=access_token") + .to_return(status: 404, body: "", headers: {}) + request expect(response).to have_gitlab_http_status(:unprocessable_entity) - expect(json_response['message']).to eq('Validation failed: Url is blocked: Only allowed schemes are http, https') + expect(json_response['message']).to include("Group import disabled on source or destination instance. " \ + "Ask an administrator to enable it on both instances and try again.") end end diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb index ee390773f29..7cea744cdb9 100644 --- a/spec/requests/api/ci/job_artifacts_spec.rb +++ b/spec/requests/api/ci/job_artifacts_spec.rb @@ -190,7 +190,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do end context 'when project is public with artifacts that are non public' do - let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) } + let(:job) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline) } it 'rejects access to artifacts' do project.update_column(:visibility_level, @@ -439,7 +439,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do context 'when public project guest and artifacts are non public' do let(:api_user) { guest } - let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) } + let(:job) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline) } before do project.update_column(:visibility_level, @@ -644,7 +644,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do end context 'when project is public with non public artifacts' do - let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline, user: api_user) } + let(:job) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline, user: api_user) } let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC } let(:public_builds) { true } diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb index 8b3ec59b785..ed0cec46a42 100644 --- a/spec/requests/api/ci/jobs_spec.rb +++ b/spec/requests/api/ci/jobs_spec.rb @@ -198,22 +198,22 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do let_it_be(:agent_authorizations_without_env) do [ - create(:agent_group_authorization, agent: create(:cluster_agent, project: other_project), group: group), - create(:agent_project_authorization, agent: create(:cluster_agent, project: project), project: project), - Clusters::Agents::ImplicitAuthorization.new(agent: create(:cluster_agent, project: project)) + create(:agent_ci_access_group_authorization, agent: create(:cluster_agent, project: other_project), group: group), + create(:agent_ci_access_project_authorization, agent: create(:cluster_agent, project: project), project: project), + Clusters::Agents::Authorizations::CiAccess::ImplicitAuthorization.new(agent: create(:cluster_agent, project: project)) ] end let_it_be(:agent_authorizations_with_review_and_production_env) do [ create( - :agent_group_authorization, + :agent_ci_access_group_authorization, agent: create(:cluster_agent, project: other_project), group: group, environments: ['production', 'review/*'] ), create( - :agent_project_authorization, + :agent_ci_access_project_authorization, agent: create(:cluster_agent, project: project), project: project, environments: ['production', 'review/*'] @@ -224,13 +224,13 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do let_it_be(:agent_authorizations_with_staging_env) do [ create( - :agent_group_authorization, + :agent_ci_access_group_authorization, agent: create(:cluster_agent, project: other_project), group: group, environments: ['staging'] ), create( - :agent_project_authorization, + :agent_ci_access_project_authorization, agent: create(:cluster_agent, project: project), project: project, environments: ['staging'] @@ -546,40 +546,18 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do describe 'GET /projects/:id/jobs rate limited' do let(:query) { {} } - context 'with the ci_enforce_rate_limits_jobs_api feature flag on' do - before do - stub_feature_flags(ci_enforce_rate_limits_jobs_api: true) - - allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy| - threshold = Gitlab::ApplicationRateLimiter.rate_limits[:jobs_index][:threshold] - allow(strategy).to receive(:increment).and_return(threshold + 1) - end - - get api("/projects/#{project.id}/jobs", api_user), params: query + before do + allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy| + threshold = Gitlab::ApplicationRateLimiter.rate_limits[:jobs_index][:threshold] + allow(strategy).to receive(:increment).and_return(threshold + 1) end - it 'enforces rate limits for the endpoint' do - 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.') - end + get api("/projects/#{project.id}/jobs", api_user), params: query end - context 'with the ci_enforce_rate_limits_jobs_api feature flag off' do - before do - stub_feature_flags(ci_enforce_rate_limits_jobs_api: false) - - allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy| - threshold = Gitlab::ApplicationRateLimiter.rate_limits[:jobs_index][:threshold] - allow(strategy).to receive(:increment).and_return(threshold + 1) - end - - get api("/projects/#{project.id}/jobs", api_user), params: query - end - - it 'makes a successful request' do - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_limited_pagination_headers - end + it 'enforces rate limits for the endpoint' do + 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.') end end diff --git a/spec/requests/api/ci/pipeline_schedules_spec.rb b/spec/requests/api/ci/pipeline_schedules_spec.rb index 2a2c5f65aee..d760e4ddf28 100644 --- a/spec/requests/api/ci/pipeline_schedules_spec.rb +++ b/spec/requests/api/ci/pipeline_schedules_spec.rb @@ -473,12 +473,12 @@ RSpec.describe API::Ci::PipelineSchedules, feature_category: :continuous_integra end context 'as the existing owner of the schedule' do - it 'rejects the request and leaves the schedule unchanged' do + it 'accepts the request and leaves the schedule unchanged' do expect do post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership", developer) end.not_to change { pipeline_schedule.reload.owner } - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:success) end end end diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb index 6d69da85449..869b0ec9dca 100644 --- a/spec/requests/api/ci/pipelines_spec.rb +++ b/spec/requests/api/ci/pipelines_spec.rb @@ -14,7 +14,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do let_it_be(:pipeline) do create(:ci_empty_pipeline, project: project, sha: project.commit.id, - ref: project.default_branch, user: user) + ref: project.default_branch, user: user, name: 'Build pipeline') end before do @@ -25,7 +25,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do it_behaves_like 'pipelines visibility table' context 'authorized user' do - it 'returns project pipelines' do + it 'returns project pipelines', :aggregate_failures do get api("/projects/#{project.id}/pipelines", user) expect(response).to have_gitlab_http_status(:ok) @@ -41,8 +41,44 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do it 'includes pipeline source' do get api("/projects/#{project.id}/pipelines", user) - expect(json_response.first.keys).to contain_exactly(*%w[id iid project_id sha ref status web_url created_at updated_at source]) + expect(json_response.first.keys).to contain_exactly(*%w[id iid project_id sha ref status web_url created_at updated_at source name]) end + + context 'when pipeline_name_in_api feature flag is off' do + before do + stub_feature_flags(pipeline_name_in_api: false) + end + + it 'does not include pipeline name in response and ignores name parameter' do + get api("/projects/#{project.id}/pipelines", user), params: { name: 'Chatops pipeline' } + + expect(json_response.length).to eq(1) + expect(json_response.first.keys).not_to include('name') + end + end + end + + it 'avoids N+1 queries' do + # Call to trigger any one time queries + get api("/projects/#{project.id}/pipelines", user), params: {} + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + get api("/projects/#{project.id}/pipelines", user), params: {} + end + + 3.times do + create( + :ci_empty_pipeline, + project: project, + sha: project.commit.id, + ref: project.default_branch, + user: user, + name: 'Build pipeline') + end + + expect do + get api("/projects/#{project.id}/pipelines", user), params: {} + end.not_to exceed_all_query_limit(control) end context 'when parameter is passed' do @@ -52,7 +88,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do create(:ci_pipeline, project: project, status: target) end - it 'returns matched pipelines' do + it 'returns matched pipelines', :aggregate_failures do get api("/projects/#{project.id}/pipelines", user), params: { scope: target } expect(response).to have_gitlab_http_status(:ok) @@ -303,11 +339,24 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end end end + + context 'when name is provided' do + let_it_be(:pipeline2) { create(:ci_empty_pipeline, project: project, user: user, name: 'Chatops pipeline') } + + it 'filters by name' do + get api("/projects/#{project.id}/pipelines", user), params: { name: 'Build pipeline' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq('Build pipeline') + end + end end end context 'unauthorized user' do - it 'does not return project pipelines' do + it 'does not return project pipelines', :aggregate_failures do get api("/projects/#{project.id}/pipelines", non_member) expect(response).to have_gitlab_http_status(:not_found) @@ -335,13 +384,13 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end context 'authorized user' do - it 'returns pipeline jobs' do + it 'returns pipeline jobs', :aggregate_failures do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array end - it 'returns correct values' do + it 'returns correct values', :aggregate_failures do expect(json_response).not_to be_empty expect(json_response.first['commit']['id']).to eq project.commit.id expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at) @@ -354,7 +403,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do let(:api_endpoint) { "/projects/#{project.id}/pipelines/#{pipeline.id}/jobs" } end - it 'returns pipeline data' do + it 'returns pipeline data', :aggregate_failures do json_job = json_response.first expect(json_job['pipeline']).not_to be_empty @@ -368,7 +417,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do context 'filter jobs with one scope element' do let(:query) { { 'scope' => 'pending' } } - it do + it :aggregate_failures do expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array @@ -382,7 +431,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do context 'when filtering to only running jobs' do let(:query) { { 'scope' => 'running' } } - it do + it :aggregate_failures do expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array @@ -402,7 +451,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do context 'filter jobs with array of scope elements' do let(:query) { { scope: %w(pending running) } } - it do + it :aggregate_failures do expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array end @@ -442,7 +491,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do let_it_be(:successor) { create(:ci_build, :success, name: 'build', pipeline: pipeline) } - it 'does not return retried jobs by default' do + it 'does not return retried jobs by default', :aggregate_failures do expect(json_response).to be_an Array expect(json_response.length).to eq(1) end @@ -450,7 +499,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do context 'when include_retried is false' do let(:query) { { include_retried: false } } - it 'does not return retried jobs' do + it 'does not return retried jobs', :aggregate_failures do expect(json_response).to be_an Array expect(json_response.length).to eq(1) end @@ -459,7 +508,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do context 'when include_retried is true' do let(:query) { { include_retried: true } } - it 'returns retried jobs' do + it 'returns retried jobs', :aggregate_failures do expect(json_response).to be_an Array expect(json_response.length).to eq(2) expect(json_response[0]['name']).to eq(json_response[1]['name']) @@ -469,7 +518,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end context 'no pipeline is found' do - it 'does not return jobs' do + it 'does not return jobs', :aggregate_failures do get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/jobs", user) expect(json_response['message']).to eq '404 Project Not Found' @@ -481,7 +530,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do context 'when user is not logged in' do let(:api_user) { nil } - it 'does not return jobs' do + it 'does not return jobs', :aggregate_failures do expect(json_response['message']).to eq '404 Project Not Found' expect(response).to have_gitlab_http_status(:not_found) end @@ -523,13 +572,13 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end context 'authorized user' do - it 'returns pipeline bridges' do + it 'returns pipeline bridges', :aggregate_failures do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array end - it 'returns correct values' do + it 'returns correct values', :aggregate_failures do expect(json_response).not_to be_empty expect(json_response.first['commit']['id']).to eq project.commit.id expect(json_response.first['id']).to eq bridge.id @@ -537,7 +586,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do expect(json_response.first['stage']).to eq bridge.stage end - it 'returns pipeline data' do + it 'returns pipeline data', :aggregate_failures do json_bridge = json_response.first expect(json_bridge['pipeline']).not_to be_empty @@ -548,7 +597,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do expect(json_bridge['pipeline']['status']).to eq bridge.pipeline.status end - it 'returns downstream pipeline data' do + it 'returns downstream pipeline data', :aggregate_failures do json_bridge = json_response.first expect(json_bridge['downstream_pipeline']).not_to be_empty @@ -568,7 +617,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do context 'with one scope element' do let(:query) { { 'scope' => 'pending' } } - it :skip_before_request do + it :skip_before_request, :aggregate_failures do get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query expect(response).to have_gitlab_http_status(:ok) @@ -581,7 +630,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do context 'with array of scope elements' do let(:query) { { scope: %w(pending running) } } - it :skip_before_request do + it :skip_before_request, :aggregate_failures do get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query expect(response).to have_gitlab_http_status(:ok) @@ -635,7 +684,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end context 'no pipeline is found' do - it 'does not return bridges' do + it 'does not return bridges', :aggregate_failures do get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/bridges", user) expect(json_response['message']).to eq '404 Project Not Found' @@ -647,7 +696,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do context 'when user is not logged in' do let(:api_user) { nil } - it 'does not return bridges' do + it 'does not return bridges', :aggregate_failures do expect(json_response['message']).to eq '404 Project Not Found' expect(response).to have_gitlab_http_status(:not_found) end @@ -704,7 +753,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do stub_ci_pipeline_to_return_yaml_file end - it 'creates and returns a new pipeline' do + it 'creates and returns a new pipeline', :aggregate_failures do expect do post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch } end.to change { project.ci_pipelines.count }.by(1) @@ -717,7 +766,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do context 'variables given' do let(:variables) { [{ 'variable_type' => 'file', 'key' => 'UPLOAD_TO_S3', 'value' => 'true' }] } - it 'creates and returns a new pipeline using the given variables' do + it 'creates and returns a new pipeline using the given variables', :aggregate_failures do expect do post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch, variables: variables } end.to change { project.ci_pipelines.count }.by(1) @@ -738,7 +787,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do stub_ci_pipeline_yaml_file(config) end - it 'creates and returns a new pipeline using the given variables' do + it 'creates and returns a new pipeline using the given variables', :aggregate_failures do expect do post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch, variables: variables } end.to change { project.ci_pipelines.count }.by(1) @@ -763,7 +812,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end end - it 'fails when using an invalid ref' do + it 'fails when using an invalid ref', :aggregate_failures do post api("/projects/#{project.id}/pipeline", user), params: { ref: 'invalid_ref' } expect(response).to have_gitlab_http_status(:bad_request) @@ -778,7 +827,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do project.update!(auto_devops_attributes: { enabled: false }) end - it 'fails to create pipeline' do + it 'fails to create pipeline', :aggregate_failures do post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch } expect(response).to have_gitlab_http_status(:bad_request) @@ -790,7 +839,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end context 'unauthorized user' do - it 'does not create pipeline' do + it 'does not create pipeline', :aggregate_failures do post api("/projects/#{project.id}/pipeline", non_member), params: { ref: project.default_branch } expect(response).to have_gitlab_http_status(:not_found) @@ -811,21 +860,22 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end context 'authorized user' do - it 'exposes known attributes' do + it 'exposes known attributes', :aggregate_failures do get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('public_api/v4/pipeline/detail') end - it 'returns project pipeline' do + it 'returns project pipeline', :aggregate_failures do get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) expect(response).to have_gitlab_http_status(:ok) expect(json_response['sha']).to match(/\A\h{40}\z/) + expect(json_response['name']).to eq('Build pipeline') end - it 'returns 404 when it does not exist' do + it 'returns 404 when it does not exist', :aggregate_failures do get api("/projects/#{project.id}/pipelines/#{non_existing_record_id}", user) expect(response).to have_gitlab_http_status(:not_found) @@ -844,10 +894,23 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do expect(json_response["coverage"]).to eq('30.00') end end + + context 'with pipeline_name_in_api disabled' do + before do + stub_feature_flags(pipeline_name_in_api: false) + end + + it 'does not return name', :aggregate_failures do + get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.keys).not_to include('name') + end + end end context 'unauthorized user' do - it 'does not return a project pipeline' do + it 'does not return a project pipeline', :aggregate_failures do get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member) expect(response).to have_gitlab_http_status(:not_found) @@ -863,7 +926,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do create(:ci_pipeline, source: dangling_source, project: project) end - it 'returns the specified pipeline' do + it 'returns the specified pipeline', :aggregate_failures do get api("/projects/#{project.id}/pipelines/#{dangling_pipeline.id}", user) expect(response).to have_gitlab_http_status(:ok) @@ -878,7 +941,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do let!(:second_pipeline) do create(:ci_empty_pipeline, project: project, sha: second_branch.target, - ref: second_branch.name, user: user) + ref: second_branch.name, user: user, name: 'Build pipeline') end before do @@ -887,18 +950,19 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end context 'default repository branch' do - it 'gets the latest pipleine' do + it 'gets the latest pipleine', :aggregate_failures do get api("/projects/#{project.id}/pipelines/latest", user) expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('public_api/v4/pipeline/detail') expect(json_response['ref']).to eq(project.default_branch) expect(json_response['sha']).to eq(project.commit.id) + expect(json_response['name']).to eq('Build pipeline') end end context 'ref parameter' do - it 'gets the latest pipleine' do + it 'gets the latest pipleine', :aggregate_failures do get api("/projects/#{project.id}/pipelines/latest", user), params: { ref: second_branch.name } expect(response).to have_gitlab_http_status(:ok) @@ -907,10 +971,23 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do expect(json_response['sha']).to eq(second_branch.target) end end + + context 'with pipeline_name_in_api disabled' do + before do + stub_feature_flags(pipeline_name_in_api: false) + end + + it 'does not return name', :aggregate_failures do + get api("/projects/#{project.id}/pipelines/latest", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.keys).not_to include('name') + end + end end context 'unauthorized user' do - it 'does not return a project pipeline' do + it 'does not return a project pipeline', :aggregate_failures do get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member) expect(response).to have_gitlab_http_status(:not_found) @@ -926,7 +1003,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do let(:api_user) { user } context 'user is a mantainer' do - it 'returns pipeline variables empty' do + it 'returns pipeline variables empty', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:ok) @@ -936,7 +1013,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do context 'with variables' do let!(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'foo', value: 'bar') } - it 'returns pipeline variables' do + it 'returns pipeline variables', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:ok) @@ -962,7 +1039,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do let(:api_user) { pipeline_owner_user } let!(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'foo', value: 'bar') } - it 'returns pipeline variables' do + it 'returns pipeline variables', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:ok) @@ -987,7 +1064,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end context 'user is not a project member' do - it 'does not return pipeline variables' do + it 'does not return pipeline variables', :aggregate_failures do get api("/projects/#{project.id}/pipelines/#{pipeline.id}/variables", non_member) expect(response).to have_gitlab_http_status(:not_found) @@ -1000,14 +1077,14 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do context 'authorized user' do let(:owner) { project.first_owner } - it 'destroys the pipeline' do + it 'destroys the pipeline', :aggregate_failures do delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner) expect(response).to have_gitlab_http_status(:no_content) expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound) end - it 'returns 404 when it does not exist' do + it 'returns 404 when it does not exist', :aggregate_failures do delete api("/projects/#{project.id}/pipelines/#{non_existing_record_id}", owner) expect(response).to have_gitlab_http_status(:not_found) @@ -1021,7 +1098,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do context 'when the pipeline has jobs' do let_it_be(:build) { create(:ci_build, project: project, pipeline: pipeline) } - it 'destroys associated jobs' do + it 'destroys associated jobs', :aggregate_failures do delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner) expect(response).to have_gitlab_http_status(:no_content) @@ -1044,7 +1121,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do context 'unauthorized user' do context 'when user is not member' do - it 'returns a 404' do + it 'returns a 404', :aggregate_failures do delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member) expect(response).to have_gitlab_http_status(:not_found) @@ -1059,7 +1136,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do project.add_developer(developer) end - it 'returns a 403' do + it 'returns a 403', :aggregate_failures do delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", developer) expect(response).to have_gitlab_http_status(:forbidden) @@ -1078,7 +1155,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do let_it_be(:build) { create(:ci_build, :failed, pipeline: pipeline) } - it 'retries failed builds' do + it 'retries failed builds', :aggregate_failures do expect do post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user) end.to change { pipeline.builds.count }.from(1).to(2) @@ -1089,7 +1166,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end context 'unauthorized user' do - it 'does not return a project pipeline' do + it 'does not return a project pipeline', :aggregate_failures do post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member) expect(response).to have_gitlab_http_status(:not_found) @@ -1106,7 +1183,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end end - it 'returns error' do + it 'returns error', :aggregate_failures do post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user) expect(response).to have_gitlab_http_status(:forbidden) @@ -1124,7 +1201,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do let_it_be(:build) { create(:ci_build, :running, pipeline: pipeline) } - context 'authorized user' do + context 'authorized user', :aggregate_failures do it 'retries failed builds', :sidekiq_might_not_need_inline do post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user) @@ -1140,7 +1217,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do project.add_reporter(reporter) end - it 'rejects the action' do + it 'rejects the action', :aggregate_failures do post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter) expect(response).to have_gitlab_http_status(:forbidden) @@ -1156,7 +1233,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do let(:pipeline) { create(:ci_pipeline, project: project) } context 'when pipeline does not have a test report' do - it 'returns an empty test report' do + it 'returns an empty test report', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:ok) @@ -1167,7 +1244,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do context 'when pipeline has a test report' do let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) } - it 'returns the test report' do + it 'returns the test report', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:ok) @@ -1180,7 +1257,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do create(:ci_build, :broken_test_reports, name: 'rspec', pipeline: pipeline) end - it 'returns a suite_error' do + it 'returns a suite_error', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:ok) @@ -1190,7 +1267,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end context 'unauthorized user' do - it 'does not return project pipelines' do + it 'does not return project pipelines', :aggregate_failures do get api("/projects/#{project.id}/pipelines/#{pipeline.id}/test_report", non_member) expect(response).to have_gitlab_http_status(:not_found) @@ -1208,7 +1285,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do let(:pipeline) { create(:ci_pipeline, project: project) } context 'when pipeline does not have a test report summary' do - it 'returns an empty test report summary' do + it 'returns an empty test report summary', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:ok) @@ -1219,7 +1296,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do context 'when pipeline has a test report summary' do let(:pipeline) { create(:ci_pipeline, :with_report_results, project: project) } - it 'returns the test report summary' do + it 'returns the test report summary', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:ok) @@ -1229,7 +1306,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do end context 'unauthorized user' do - it 'does not return project pipelines' do + it 'does not return project pipelines', :aggregate_failures do get api("/projects/#{project.id}/pipelines/#{pipeline.id}/test_report_summary", non_member) expect(response).to have_gitlab_http_status(:not_found) diff --git a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb index 3d3d699542b..596af1110cc 100644 --- a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb +++ b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb @@ -174,8 +174,21 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego expect(json_response['RemoteObject']).to have_key('StoreURL') expect(json_response['RemoteObject']).to have_key('DeleteURL') expect(json_response['RemoteObject']).to have_key('MultipartUpload') + expect(json_response['RemoteObject']['SkipDelete']).to eq(true) expect(json_response['MaximumSize']).not_to be_nil end + + context 'when ci_artifacts_upload_to_final_location flag is disabled' do + before do + stub_feature_flags(ci_artifacts_upload_to_final_location: false) + end + + it 'does not skip delete' do + subject + + expect(json_response['RemoteObject']['SkipDelete']).to eq(false) + end + end end context 'when direct upload is disabled' do @@ -255,8 +268,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego it 'tracks code_intelligence usage ping' do tracking_params = { event_names: 'i_source_code_code_intelligence', - start_date: Date.yesterday, - end_date: Date.today + start_date: Date.today.beginning_of_week, + end_date: 1.week.from_now } expect { authorize_artifacts_with_token_in_headers(artifact_type: :lsif) } @@ -374,29 +387,53 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego let(:object) do fog_connection.directories.new(key: 'artifacts').files.create( # rubocop:disable Rails/SaveBang - key: 'tmp/uploads/12312300', + key: remote_path, body: 'content' ) end let(:file_upload) { fog_to_uploaded_file(object) } - before do - upload_artifacts(file_upload, headers_with_token, 'file.remote_id' => remote_id) - end + context 'when uploaded file has matching pending remote upload to its final location' do + let(:remote_path) { '12345/foo-bar-123' } + let(:object_remote_id) { remote_path } + let(:remote_id) { remote_path } + + before do + allow(JobArtifactUploader).to receive(:generate_final_store_path).and_return(remote_path) - context 'when valid remote_id is used' do - let(:remote_id) { '12312300' } + ObjectStorage::PendingDirectUpload.prepare( + JobArtifactUploader.storage_location_identifier, + remote_path + ) + + upload_artifacts(file_upload, headers_with_token, 'file.remote_id' => remote_path) + end it_behaves_like 'successful artifacts upload' end - context 'when invalid remote_id is used' do - let(:remote_id) { 'invalid id' } + context 'when uploaded file is uploaded to temporary location' do + let(:object_remote_id) { JobArtifactUploader.generate_remote_id } + let(:remote_path) { File.join(ObjectStorage::TMP_UPLOAD_PATH, object_remote_id) } + + before do + upload_artifacts(file_upload, headers_with_token, 'file.remote_id' => remote_id) + end + + context 'and matching temporary remote_id is used' do + let(:remote_id) { object_remote_id } + + it_behaves_like 'successful artifacts upload' + end + + context 'and invalid remote_id is used' do + let(:remote_id) { JobArtifactUploader.generate_remote_id } - it 'responds with bad request' do - expect(response).to have_gitlab_http_status(:internal_server_error) - expect(json_response['message']).to eq("Missing file") + it 'responds with internal server error' do + expect(response).to have_gitlab_http_status(:internal_server_error) + expect(json_response['message']).to eq("Missing file") + end end end end diff --git a/spec/requests/api/ci/runner/jobs_put_spec.rb b/spec/requests/api/ci/runner/jobs_put_spec.rb index ef3b38e3fc4..ab7ab4e74f8 100644 --- a/spec/requests/api/ci/runner/jobs_put_spec.rb +++ b/spec/requests/api/ci/runner/jobs_put_spec.rb @@ -21,13 +21,13 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego let_it_be(:project) { create(:project, namespace: group, shared_runners_enabled: false) } let_it_be(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') } let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) } - let_it_be(:runner_machine) { create(:ci_runner_machine, runner: runner) } + let_it_be(:runner_manager) { create(:ci_runner_machine, runner: runner) } let_it_be(:user) { create(:user) } describe 'PUT /api/v4/jobs/:id' do let_it_be_with_reload(:job) do create(:ci_build, :pending, :trace_live, pipeline: pipeline, project: project, user: user, - runner_id: runner.id, runner_machine: runner_machine) + runner_id: runner.id, runner_manager: runner_manager) end before do @@ -40,7 +40,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego it 'updates runner info' do expect { update_job(state: 'success') }.to change { runner.reload.contacted_at } - .and change { runner_machine.reload.contacted_at } + .and change { runner_manager.reload.contacted_at } end context 'when status is given' do 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 6e721d40560..0164eda7680 100644 --- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb +++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb @@ -122,56 +122,33 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego context 'when system_id parameter is specified' do subject(:request) { request_job(**args) } - context 'with create_runner_machine FF enabled' do - before do - stub_feature_flags(create_runner_machine: true) - end - - context 'when ci_runner_machines with same system_xid does not exist' do - let(:args) { { system_id: 's_some_system_id' } } - - it 'creates respective ci_runner_machines record', :freeze_time do - expect { request }.to change { runner.runner_machines.reload.count }.from(0).to(1) - - machine = runner.runner_machines.last - expect(machine.system_xid).to eq args[:system_id] - expect(machine.runner).to eq runner - expect(machine.contacted_at).to eq Time.current - end - end - - context 'when ci_runner_machines with same system_xid already exists', :freeze_time do - let(:args) { { system_id: 's_existing_system_id' } } - let!(:runner_machine) do - create(:ci_runner_machine, runner: runner, system_xid: args[:system_id], contacted_at: 1.hour.ago) - end - - it 'does not create new ci_runner_machines record' do - expect { request }.not_to change { Ci::RunnerMachine.count } - end + context 'when ci_runner_machines with same system_xid does not exist' do + let(:args) { { system_id: 's_some_system_id' } } - it 'updates the contacted_at field' do - request + it 'creates respective ci_runner_machines record', :freeze_time do + expect { request }.to change { runner.runner_managers.reload.count }.from(0).to(1) - expect(runner_machine.reload.contacted_at).to eq Time.current - end + runner_manager = runner.runner_managers.last + expect(runner_manager.system_xid).to eq args[:system_id] + expect(runner_manager.runner).to eq runner + expect(runner_manager.contacted_at).to eq Time.current end end - context 'with create_runner_machine FF disabled' do - before do - stub_feature_flags(create_runner_machine: false) + context 'when ci_runner_machines with same system_xid already exists', :freeze_time do + let(:args) { { system_id: 's_existing_system_id' } } + let!(:runner_manager) do + create(:ci_runner_machine, runner: runner, system_xid: args[:system_id], contacted_at: 1.hour.ago) end - context 'when ci_runner_machines with same system_xid does not exist' do - let(:args) { { system_id: 's_some_system_id' } } + it 'does not create new ci_runner_machines record' do + expect { request }.not_to change { Ci::RunnerManager.count } + end - it 'does not create respective ci_runner_machines record', :freeze_time, :aggregate_failures do - expect { request }.not_to change { runner.runner_machines.reload.count } + it 'updates the contacted_at field' do + request - expect(response).to have_gitlab_http_status(:created) - expect(runner.runner_machines).to be_empty - end + expect(runner_manager.reload.contacted_at).to eq Time.current end end end @@ -253,11 +230,14 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego end let(:expected_cache) do - [{ 'key' => a_string_matching(/^cache_key-(?>protected|non_protected)$/), - 'untracked' => false, - 'paths' => ['vendor/*'], - 'policy' => 'pull-push', - 'when' => 'on_success' }] + [{ + 'key' => a_string_matching(/^cache_key-(?>protected|non_protected)$/), + 'untracked' => false, + 'paths' => ['vendor/*'], + 'policy' => 'pull-push', + 'when' => 'on_success', + 'fallback_keys' => [] + }] end let(:expected_features) do @@ -366,36 +346,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego end end - context 'when job filtered by job_age' do - let!(:job) do - create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, queued_at: 60.seconds.ago) - end - - before do - job.queuing_entry&.update!(created_at: 60.seconds.ago) - end - - context 'job is queued less than job_age parameter' do - let(:job_age) { 120 } - - it 'gives 204' do - request_job(job_age: job_age) - - expect(response).to have_gitlab_http_status(:no_content) - end - end - - context 'job is queued more than job_age parameter' do - let(:job_age) { 30 } - - it 'picks a job' do - request_job(job_age: job_age) - - expect(response).to have_gitlab_http_status(:created) - end - end - end - context 'when job is made for branch' do it 'sets tag as ref_type' do request_job @@ -831,19 +781,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego end end end - - context 'when the FF ci_hooks_pre_get_sources_script is disabled' do - before do - stub_feature_flags(ci_hooks_pre_get_sources_script: false) - end - - it 'does not return the pre_get_sources_script' do - request_job - - expect(response).to have_gitlab_http_status(:created) - expect(json_response).not_to have_key('hooks') - end - end end describe 'port support' do diff --git a/spec/requests/api/ci/runner/runners_delete_spec.rb b/spec/requests/api/ci/runner/runners_delete_spec.rb index 65c287a9535..681dd4d701e 100644 --- a/spec/requests/api/ci/runner/runners_delete_spec.rb +++ b/spec/requests/api/ci/runner/runners_delete_spec.rb @@ -7,16 +7,19 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego include RedisHelpers include WorkhorseHelpers - let(:registration_token) { 'abcdefg123456' } - before do stub_feature_flags(ci_enable_live_trace: true) stub_gitlab_calls - stub_application_setting(runners_registration_token: registration_token) - allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes) + allow_next_instance_of(::Ci::Runner) { |runner| allow(runner).to receive(:cache_attributes) } end describe '/api/v4/runners' do + let(:registration_token) { 'abcdefg123456' } + + before do + stub_application_setting(runners_registration_token: registration_token) + end + describe 'DELETE /api/v4/runners' do context 'when no token is provided' do it 'returns 400 error' do @@ -57,4 +60,85 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego end end end + + describe '/api/v4/runners/managers' do + describe 'DELETE /api/v4/runners/managers' do + subject(:delete_request) { delete api('/runners/managers'), params: delete_params } + + context 'with created runner' do + let!(:runner) { create(:ci_runner, :with_runner_manager, registration_type: :authenticated_user) } + + context 'with matching system_id' do + context 'when no token is provided' do + let(:delete_params) { { system_id: runner.runner_managers.first.system_xid } } + + it 'returns 400 error' do + delete_request + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'when invalid token is provided' do + let(:delete_params) { { token: 'invalid', system_id: runner.runner_managers.first.system_xid } } + + it 'returns 403 error' do + delete_request + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + end + + context 'when valid token is provided' do + context 'with created runner' do + let!(:runner) { create(:ci_runner, :with_runner_manager, registration_type: :authenticated_user) } + + context 'with matching system_id' do + let(:delete_params) { { token: runner.token, system_id: runner.runner_managers.first.system_xid } } + + it 'deletes runner manager' do + expect do + delete_request + + expect(response).to have_gitlab_http_status(:no_content) + end.to change { runner.runner_managers.count }.from(1).to(0) + + expect(::Ci::Runner.count).to eq(1) + end + + it_behaves_like '412 response' do + let(:request) { api('/runners/managers') } + let(:params) { delete_params } + end + + it_behaves_like 'storing arguments in the application context for the API' do + let(:expected_params) { { client_id: "runner/#{runner.id}" } } + end + end + + context 'with unknown system_id' do + let(:delete_params) { { token: runner.token, system_id: 'unknown_system_id' } } + + it 'returns 404 error' do + delete_request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'without system_id' do + let(:delete_params) { { token: runner.token } } + + it 'does not delete runner manager nor runner' do + delete_request + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end + end + end + 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 73f8e87a9fb..a36ea2115cf 100644 --- a/spec/requests/api/ci/runner/runners_post_spec.rb +++ b/spec/requests/api/ci/runner/runners_post_spec.rb @@ -15,14 +15,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego context 'when invalid token is provided' do it 'returns 403 error' do - allow_next_instance_of(::Ci::Runners::RegisterRunnerService) do |service| - allow(service).to receive(:execute) - .and_return(ServiceResponse.error(message: 'invalid token supplied', http_status: :forbidden)) - end - post api('/runners'), params: { token: 'invalid' } expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['message']).to eq('403 Forbidden - invalid token supplied') end end @@ -44,21 +40,24 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego let_it_be(:new_runner) { create(:ci_runner) } before do - allow_next_instance_of(::Ci::Runners::RegisterRunnerService) do |service| - expected_params = { - description: 'server.hostname', - maintenance_note: 'Some maintainer notes', - run_untagged: false, - tag_list: %w(tag1 tag2), - locked: true, - active: true, - access_level: 'ref_protected', - maximum_timeout: 9000 - }.stringify_keys + expected_params = { + description: 'server.hostname', + maintenance_note: 'Some maintainer notes', + run_untagged: false, + tag_list: %w(tag1 tag2), + locked: true, + active: true, + access_level: 'ref_protected', + maximum_timeout: 9000 + }.stringify_keys + allow_next_instance_of( + ::Ci::Runners::RegisterRunnerService, + 'valid token', + a_hash_including(expected_params) + ) do |service| expect(service).to receive(:execute) .once - .with('valid token', a_hash_including(expected_params)) .and_return(ServiceResponse.success(payload: { runner: new_runner })) end end @@ -109,11 +108,14 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego let(:new_runner) { create(:ci_runner) } it 'converts to maintenance_note param' do - allow_next_instance_of(::Ci::Runners::RegisterRunnerService) do |service| + allow_next_instance_of( + ::Ci::Runners::RegisterRunnerService, + 'valid token', + a_hash_including('maintenance_note' => 'Some maintainer notes') + .and(excluding('maintainter_note' => anything)) + ) do |service| expect(service).to receive(:execute) .once - .with('valid token', a_hash_including('maintenance_note' => 'Some maintainer notes') - .and(excluding('maintainter_note' => anything))) .and_return(ServiceResponse.success(payload: { runner: new_runner })) end @@ -134,12 +136,13 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego let_it_be(:new_runner) { build(:ci_runner) } it 'uses active value in registration' do - expect_next_instance_of(::Ci::Runners::RegisterRunnerService) do |service| - expected_params = { active: false }.stringify_keys - + expect_next_instance_of( + ::Ci::Runners::RegisterRunnerService, + 'valid token', + a_hash_including({ active: false }.stringify_keys) + ) do |service| expect(service).to receive(:execute) .once - .with('valid token', a_hash_including(expected_params)) .and_return(ServiceResponse.success(payload: { runner: new_runner })) end @@ -197,12 +200,13 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego let(:tag_list) { (1..::Ci::Runner::TAG_LIST_MAX_LENGTH + 1).map { |i| "tag#{i}" } } it 'uses tag_list value in registration and returns error' do - expect_next_instance_of(::Ci::Runners::RegisterRunnerService) do |service| - expected_params = { tag_list: tag_list }.stringify_keys - + expect_next_instance_of( + ::Ci::Runners::RegisterRunnerService, + registration_token, + a_hash_including({ tag_list: tag_list }.stringify_keys) + ) do |service| expect(service).to receive(:execute) .once - .with(registration_token, a_hash_including(expected_params)) .and_call_original end @@ -217,12 +221,13 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego let(:tag_list) { (1..20).map { |i| "tag#{i}" } } it 'uses tag_list value in registration and successfully creates runner' do - expect_next_instance_of(::Ci::Runners::RegisterRunnerService) do |service| - expected_params = { tag_list: tag_list }.stringify_keys - + expect_next_instance_of( + ::Ci::Runners::RegisterRunnerService, + registration_token, + a_hash_including({ tag_list: tag_list }.stringify_keys) + ) do |service| expect(service).to receive(:execute) .once - .with(registration_token, a_hash_including(expected_params)) .and_call_original end @@ -232,6 +237,18 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego end end end + + context 'when runner registration is disallowed' do + before do + stub_application_setting(allow_runner_registration_token: false) + end + + it 'returns 410 Gone status' do + post api('/runners'), params: { token: registration_token } + + expect(response).to have_gitlab_http_status(:gone) + end + end end end end 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 a6a1ad947aa..f1b33826f5e 100644 --- a/spec/requests/api/ci/runner/runners_verify_post_spec.rb +++ b/spec/requests/api/ci/runner/runners_verify_post_spec.rb @@ -17,7 +17,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego end describe '/api/v4/runners' do - describe 'POST /api/v4/runners/verify' do + describe 'POST /api/v4/runners/verify', :freeze_time do let_it_be_with_reload(:runner) { create(:ci_runner, token_expires_at: 3.days.from_now) } let(:params) {} @@ -45,9 +45,12 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego context 'when valid token is provided' do let(:params) { { token: runner.token } } - context 'with create_runner_machine FF enabled' do - before do - stub_feature_flags(create_runner_machine: true) + context 'with glrt-prefixed token' do + let_it_be(:registration_token) { 'glrt-abcdefg123456' } + let_it_be(:registration_type) { :authenticated_user } + let_it_be(:runner) do + create(:ci_runner, registration_type: registration_type, + token: registration_token, token_expires_at: 3.days.from_now) end it 'verifies Runner credentials' do @@ -61,39 +64,29 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego }) end - context 'with non-expiring runner token' do - before do - runner.update!(token_expires_at: nil) - end - - it 'verifies Runner credentials' do - verify - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ - 'id' => runner.id, - 'token' => runner.token, - 'token_expires_at' => nil - }) - end + it 'does not update contacted_at' do + expect { verify }.not_to change { runner.reload.contacted_at }.from(nil) end + end - it_behaves_like 'storing arguments in the application context for the API' do - let(:expected_params) { { client_id: "runner/#{runner.id}" } } - end + it 'verifies Runner credentials' do + verify - context 'when system_id is provided' do - let(:params) { { token: runner.token, system_id: 's_some_system_id' } } + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ + 'id' => runner.id, + 'token' => runner.token, + 'token_expires_at' => runner.token_expires_at.iso8601(3) + }) + end - it 'creates a runner_machine' do - expect { verify }.to change { Ci::RunnerMachine.count }.by(1) - end - end + it 'updates contacted_at' do + expect { verify }.to change { runner.reload.contacted_at }.from(nil).to(Time.current) end - context 'with create_runner_machine FF disabled' do + context 'with non-expiring runner token' do before do - stub_feature_flags(create_runner_machine: false) + runner.update!(token_expires_at: nil) end it 'verifies Runner credentials' do @@ -103,18 +96,20 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego expect(json_response).to eq({ 'id' => runner.id, 'token' => runner.token, - 'token_expires_at' => runner.token_expires_at.iso8601(3) + 'token_expires_at' => nil }) end + end - context 'when system_id is provided' do - let(:params) { { token: runner.token, system_id: 's_some_system_id' } } + it_behaves_like 'storing arguments in the application context for the API' do + let(:expected_params) { { client_id: "runner/#{runner.id}" } } + end - it 'does not create a runner_machine', :aggregate_failures do - expect { verify }.not_to change { Ci::RunnerMachine.count } + context 'when system_id is provided' do + let(:params) { { token: runner.token, system_id: 's_some_system_id' } } - expect(response).to have_gitlab_http_status(:ok) - end + it 'creates a runner_manager' do + expect { verify }.to change { Ci::RunnerManager.count }.by(1) end 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 1110dbf5fbc..98edde93e95 100644 --- a/spec/requests/api/ci/runners_reset_registration_token_spec.rb +++ b/spec/requests/api/ci/runners_reset_registration_token_spec.rb @@ -3,10 +3,12 @@ require 'spec_helper' RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do - subject { post api("#{prefix}/runners/reset_registration_token", user) } + let_it_be(:admin_mode) { false } + + subject { post api("#{prefix}/runners/reset_registration_token", user, admin_mode: admin_mode) } shared_examples 'bad request' do |result| - it 'returns 400 error' do + it 'returns 400 error', :aggregate_failures do expect { subject }.not_to change { get_token } expect(response).to have_gitlab_http_status(:bad_request) @@ -15,7 +17,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end shared_examples 'unauthenticated' do - it 'returns 401 error' do + it 'returns 401 error', :aggregate_failures do expect { subject }.not_to change { get_token } expect(response).to have_gitlab_http_status(:unauthorized) @@ -23,7 +25,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end shared_examples 'unauthorized' do - it 'returns 403 error' do + it 'returns 403 error', :aggregate_failures do expect { subject }.not_to change { get_token } expect(response).to have_gitlab_http_status(:forbidden) @@ -31,7 +33,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end shared_examples 'not found' do |scope| - it 'returns 404 error' do + it 'returns 404 error', :aggregate_failures do expect { subject }.not_to change { get_token } expect(response).to have_gitlab_http_status(:not_found) @@ -58,7 +60,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end shared_context 'when authorized' do |scope| - it 'resets runner registration token' do + it 'resets runner registration token', :aggregate_failures do expect { subject }.to change { get_token } expect(response).to have_gitlab_http_status(:success) @@ -99,6 +101,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do include_context 'when authorized', 'instance' do let_it_be(:user) { create(:user, :admin) } + let_it_be(:admin_mode) { true } def get_token ApplicationSetting.current_without_cache.runners_registration_token diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb index ca051386265..2b2d2e0def8 100644 --- a/spec/requests/api/ci/runners_spec.rb +++ b/spec/requests/api/ci/runners_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do +RSpec.describe API::Ci::Runners, :aggregate_failures, feature_category: :runner_fleet do let_it_be(:admin) { create(:user, :admin) } let_it_be(:user) { create(:user) } let_it_be(:user2) { create(:user) } @@ -134,17 +134,21 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end describe 'GET /runners/all' do + let(:path) { '/runners/all' } + + it_behaves_like 'GET request permissions for admin mode' + context 'authorized user' do context 'with admin privileges' do it 'returns response status and headers' do - get api('/runners/all', admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers end it 'returns all runners' do - get api('/runners/all', admin) + get api(path, admin, admin_mode: true) expect(json_response).to match_array [ a_hash_including('description' => 'Project runner', 'is_shared' => false, 'active' => true, 'paused' => false, 'runner_type' => 'project_type'), @@ -156,7 +160,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end it 'filters runners by scope' do - get api('/runners/all?scope=shared', admin) + get api('/runners/all?scope=shared', admin, admin_mode: true) shared = json_response.all? { |r| r['is_shared'] } expect(response).to have_gitlab_http_status(:ok) @@ -167,7 +171,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end it 'filters runners by scope' do - get api('/runners/all?scope=specific', admin) + get api('/runners/all?scope=specific', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -181,12 +185,12 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end it 'avoids filtering if scope is invalid' do - get api('/runners/all?scope=unknown', admin) + get api('/runners/all?scope=unknown', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) end it 'filters runners by project type' do - get api('/runners/all?type=project_type', admin) + get api('/runners/all?type=project_type', admin, admin_mode: true) expect(json_response).to match_array [ a_hash_including('description' => 'Project runner'), @@ -195,7 +199,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end it 'filters runners by group type' do - get api('/runners/all?type=group_type', admin) + get api('/runners/all?type=group_type', admin, admin_mode: true) expect(json_response).to match_array [ a_hash_including('description' => 'Group runner A'), @@ -204,7 +208,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end it 'does not filter by invalid type' do - get api('/runners/all?type=bogus', admin) + get api('/runners/all?type=bogus', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) end @@ -213,7 +217,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do let_it_be(:runner) { create(:ci_runner, :project, :inactive, description: 'Inactive project runner', projects: [project]) } it 'filters runners by status' do - get api('/runners/all?paused=true', admin) + get api('/runners/all?paused=true', admin, admin_mode: true) expect(json_response).to match_array [ a_hash_including('description' => 'Inactive project runner') @@ -221,7 +225,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end it 'filters runners by status' do - get api('/runners/all?status=paused', admin) + get api('/runners/all?status=paused', admin, admin_mode: true) expect(json_response).to match_array [ a_hash_including('description' => 'Inactive project runner') @@ -230,7 +234,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end it 'does not filter by invalid status' do - get api('/runners/all?status=bogus', admin) + get api('/runners/all?status=bogus', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) end @@ -239,7 +243,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do create(:ci_runner, :project, description: 'Runner tagged with tag1 and tag2', projects: [project], tag_list: %w[tag1 tag2]) create(:ci_runner, :project, description: 'Runner tagged with tag2', projects: [project], tag_list: ['tag2']) - get api('/runners/all?tag_list=tag1,tag2', admin) + get api('/runners/all?tag_list=tag1,tag2', admin, admin_mode: true) expect(json_response).to match_array [ a_hash_including('description' => 'Runner tagged with tag1 and tag2') @@ -249,7 +253,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'without admin privileges' do it 'does not return runners list' do - get api('/runners/all', user) + get api(path, user) expect(response).to have_gitlab_http_status(:forbidden) end @@ -266,6 +270,10 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end describe 'GET /runners/:id' do + let(:path) { "/runners/#{project_runner.id}" } + + it_behaves_like 'GET request permissions for admin mode' + context 'admin user' do context 'when runner is shared' do it "returns runner's details" do @@ -286,7 +294,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do it 'deletes unused runner' do expect do - delete api("/runners/#{unused_project_runner.id}", admin) + delete api("/runners/#{unused_project_runner.id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:no_content) end.to change { ::Ci::Runner.project_type.count }.by(-1) @@ -294,21 +302,21 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end it "returns runner's details" do - get api("/runners/#{project_runner.id}", admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['description']).to eq(project_runner.description) end it "returns the project's details for a project runner" do - get api("/runners/#{project_runner.id}", admin) + get api(path, admin, admin_mode: true) expect(json_response['projects'].first['id']).to eq(project.id) end end it 'returns 404 if runner does not exist' do - get api('/runners/0', admin) + get api("/runners/#{non_existing_record_id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end @@ -316,7 +324,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'when the runner is a group runner' do it "returns the runner's details" do - get api("/runners/#{group_runner_a.id}", admin) + get api("/runners/#{group_runner_a.id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['description']).to eq(group_runner_a.description) @@ -327,7 +335,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context "runner project's administrative user" do context 'when runner is not shared' do it "returns runner's details" do - get api("/runners/#{project_runner.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response['description']).to eq(project_runner.description) @@ -346,7 +354,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'other authorized user' do it "does not return project runner's details" do - get api("/runners/#{project_runner.id}", user2) + get api(path, user2) expect(response).to have_gitlab_http_status(:forbidden) end @@ -354,7 +362,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'unauthorized user' do it "does not return project runner's details" do - get api("/runners/#{project_runner.id}") + get api(path) expect(response).to have_gitlab_http_status(:unauthorized) end @@ -362,6 +370,12 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end describe 'PUT /runners/:id' do + let(:path) { "/runners/#{project_runner.id}" } + + it_behaves_like 'PUT request permissions for admin mode' do + let(:params) { { description: 'test' } } + end + context 'admin user' do # see https://gitlab.com/gitlab-org/gitlab-foss/issues/48625 context 'single parameter update' do @@ -492,20 +506,22 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end it 'returns 404 if runner does not exist' do - update_runner(0, admin, description: 'test') + update_runner(non_existing_record_id, admin, description: 'test') expect(response).to have_gitlab_http_status(:not_found) end def update_runner(id, user, args) - put api("/runners/#{id}", user), params: args + put api("/runners/#{id}", user, admin_mode: true), params: args end end context 'authorized user' do + let_it_be(:params) { { description: 'test' } } + context 'when runner is shared' do it 'does not update runner' do - put api("/runners/#{shared_runner.id}", user), params: { description: 'test' } + put api("/runners/#{shared_runner.id}", user), params: params expect(response).to have_gitlab_http_status(:forbidden) end @@ -513,17 +529,16 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'when runner is not shared' do it 'does not update project runner without access to it' do - put api("/runners/#{project_runner.id}", user2), params: { description: 'test' } + put api(path, user2), params: { description: 'test' } expect(response).to have_gitlab_http_status(:forbidden) end it 'updates project runner with access to it' do description = project_runner.description - put api("/runners/#{project_runner.id}", admin), params: { description: 'test' } + put api(path, admin, admin_mode: true), params: params project_runner.reload - expect(response).to have_gitlab_http_status(:ok) expect(project_runner.description).to eq('test') expect(project_runner.description).not_to eq(description) end @@ -532,7 +547,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'unauthorized user' do it 'does not delete project runner' do - put api("/runners/#{project_runner.id}") + put api(path) expect(response).to have_gitlab_http_status(:unauthorized) end @@ -540,6 +555,10 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end describe 'DELETE /runners/:id' do + let(:path) { "/runners/#{shared_runner.id}" } + + it_behaves_like 'DELETE request permissions for admin mode' + context 'admin user' do context 'when runner is shared' do it 'deletes runner' do @@ -548,14 +567,14 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end expect do - delete api("/runners/#{shared_runner.id}", admin) + delete api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:no_content) end.to change { ::Ci::Runner.instance_type.count }.by(-1) end it_behaves_like '412 response' do - let(:request) { api("/runners/#{shared_runner.id}", admin) } + let(:request) { api(path, admin, admin_mode: true) } end end @@ -566,7 +585,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end expect do - delete api("/runners/#{project_runner.id}", admin) + delete api("/runners/#{project_runner.id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:no_content) end.to change { ::Ci::Runner.project_type.count }.by(-1) @@ -578,7 +597,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do expect(service).not_to receive(:execute) end - delete api('/runners/0', admin) + delete api("/runners/#{non_existing_record_id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end @@ -587,7 +606,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'authorized user' do context 'when runner is shared' do it 'does not delete runner' do - delete api("/runners/#{shared_runner.id}", user) + delete api(path, user) expect(response).to have_gitlab_http_status(:forbidden) end end @@ -671,10 +690,16 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end describe 'POST /runners/:id/reset_authentication_token' do + let(:path) { "/runners/#{shared_runner.id}/reset_authentication_token" } + + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { {} } + end + context 'admin user' do it 'resets shared runner authentication token' do expect do - post api("/runners/#{shared_runner.id}/reset_authentication_token", admin) + post api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:success) expect(json_response).to eq({ 'token' => shared_runner.reload.token, 'token_expires_at' => nil }) @@ -682,7 +707,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end it 'returns 404 if runner does not exist' do - post api('/runners/0/reset_authentication_token', admin) + post api("/runners/#{non_existing_record_id}/reset_authentication_token", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end @@ -765,7 +790,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'unauthorized user' do it 'does not reset authentication token' do expect do - post api("/runners/#{shared_runner.id}/reset_authentication_token") + post api(path) expect(response).to have_gitlab_http_status(:unauthorized) end.not_to change { shared_runner.reload.token } @@ -779,12 +804,15 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do let_it_be(:job_3) { create(:ci_build, :failed, runner: shared_runner, project: project) } let_it_be(:job_4) { create(:ci_build, :running, runner: project_runner, project: project) } let_it_be(:job_5) { create(:ci_build, :failed, runner: project_runner, project: project) } + let(:path) { "/runners/#{project_runner.id}/jobs" } + + it_behaves_like 'GET request permissions for admin mode' context 'admin user' do context 'when runner exists' do context 'when runner is shared' do it 'return jobs' do - get api("/runners/#{shared_runner.id}/jobs", admin) + get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -796,7 +824,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'when runner is a project runner' do it 'return jobs' do - get api("/runners/#{project_runner.id}/jobs", admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -825,7 +853,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'when valid status is provided' do it 'return filtered jobs' do - get api("/runners/#{project_runner.id}/jobs?status=failed", admin) + get api("/runners/#{project_runner.id}/jobs?status=failed", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -839,7 +867,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'when valid order_by is provided' do context 'when sort order is not specified' do it 'return jobs in descending order' do - get api("/runners/#{project_runner.id}/jobs?order_by=id", admin) + get api("/runners/#{project_runner.id}/jobs?order_by=id", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -852,7 +880,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'when sort order is specified as asc' do it 'return jobs sorted in ascending order' do - get api("/runners/#{project_runner.id}/jobs?order_by=id&sort=asc", admin) + get api("/runners/#{project_runner.id}/jobs?order_by=id&sort=asc", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -866,7 +894,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'when invalid status is provided' do it 'return 400' do - get api("/runners/#{project_runner.id}/jobs?status=non-existing", admin) + get api("/runners/#{project_runner.id}/jobs?status=non-existing", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) end @@ -874,7 +902,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'when invalid order_by is provided' do it 'return 400' do - get api("/runners/#{project_runner.id}/jobs?order_by=non-existing", admin) + get api("/runners/#{project_runner.id}/jobs?order_by=non-existing", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) end @@ -882,7 +910,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'when invalid sort is provided' do it 'return 400' do - get api("/runners/#{project_runner.id}/jobs?sort=non-existing", admin) + get api("/runners/#{project_runner.id}/jobs?sort=non-existing", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) end @@ -890,16 +918,16 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end it 'avoids N+1 DB queries' do - get api("/runners/#{shared_runner.id}/jobs", admin) + get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true) control = ActiveRecord::QueryRecorder.new do - get api("/runners/#{shared_runner.id}/jobs", admin) + get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true) end create(:ci_build, :failed, runner: shared_runner, project: project) expect do - get api("/runners/#{shared_runner.id}/jobs", admin) + get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true) end.not_to exceed_query_limit(control.count) end @@ -925,12 +953,12 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do ]).once.and_call_original end - get api("/runners/#{shared_runner.id}/jobs", admin), params: { per_page: 2, order_by: 'id', sort: 'desc' } + get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true), params: { per_page: 2, order_by: 'id', sort: 'desc' } end context "when runner doesn't exist" do it 'returns 404' do - get api('/runners/0/jobs', admin) + get api('/runners/0/jobs', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end @@ -949,7 +977,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'when runner is a project runner' do it 'return jobs' do - get api("/runners/#{project_runner.id}/jobs", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -992,7 +1020,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'other authorized user' do it 'does not return jobs' do - get api("/runners/#{project_runner.id}/jobs", user2) + get api(path, user2) expect(response).to have_gitlab_http_status(:forbidden) end @@ -1000,7 +1028,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'unauthorized user' do it 'does not return jobs' do - get api("/runners/#{project_runner.id}/jobs") + get api(path) expect(response).to have_gitlab_http_status(:unauthorized) end @@ -1028,7 +1056,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do describe 'GET /projects/:id/runners' do context 'authorized user with maintainer privileges' do it 'returns response status and headers' do - get api('/runners/all', admin) + get api('/runners/all', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -1200,19 +1228,27 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end describe 'POST /projects/:id/runners' do + let(:path) { "/projects/#{project.id}/runners" } + + it_behaves_like 'POST request permissions for admin mode' do + let!(:new_project_runner) { create(:ci_runner, :project) } + let(:params) { { runner_id: new_project_runner.id } } + let(:failed_status_code) { :not_found } + end + context 'authorized user' do let_it_be(:project_runner2) { create(:ci_runner, :project, projects: [project2]) } it 'enables project runner' do expect do - post api("/projects/#{project.id}/runners", user), params: { runner_id: project_runner2.id } + post api(path, user), params: { runner_id: project_runner2.id } end.to change { project.runners.count }.by(+1) expect(response).to have_gitlab_http_status(:created) end it 'avoids changes when enabling already enabled runner' do expect do - post api("/projects/#{project.id}/runners", user), params: { runner_id: project_runner.id } + post api(path, user), params: { runner_id: project_runner.id } end.to change { project.runners.count }.by(0) expect(response).to have_gitlab_http_status(:bad_request) end @@ -1221,20 +1257,20 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do project_runner2.update!(locked: true) expect do - post api("/projects/#{project.id}/runners", user), params: { runner_id: project_runner2.id } + post api(path, user), params: { runner_id: project_runner2.id } end.to change { project.runners.count }.by(0) expect(response).to have_gitlab_http_status(:forbidden) end it 'does not enable shared runner' do - post api("/projects/#{project.id}/runners", user), params: { runner_id: shared_runner.id } + post api(path, user), params: { runner_id: shared_runner.id } expect(response).to have_gitlab_http_status(:forbidden) end it 'does not enable group runner' do - post api("/projects/#{project.id}/runners", user), params: { runner_id: group_runner_a.id } + post api(path, user), params: { runner_id: group_runner_a.id } expect(response).to have_gitlab_http_status(:forbidden) end @@ -1245,7 +1281,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do it 'enables any project runner' do expect do - post api("/projects/#{project.id}/runners", admin), params: { runner_id: new_project_runner.id } + post api(path, admin, admin_mode: true), params: { runner_id: new_project_runner.id } end.to change { project.runners.count }.by(+1) expect(response).to have_gitlab_http_status(:created) end @@ -1257,7 +1293,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do it 'does not enable project runner' do expect do - post api("/projects/#{project.id}/runners", admin), params: { runner_id: new_project_runner.id } + post api(path, admin, admin_mode: true), params: { runner_id: new_project_runner.id } end.not_to change { project.runners.count } expect(response).to have_gitlab_http_status(:bad_request) end @@ -1266,7 +1302,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do end it 'raises an error when no runner_id param is provided' do - post api("/projects/#{project.id}/runners", admin) + post api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) end @@ -1276,7 +1312,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do let!(:new_project_runner) { create(:ci_runner, :project) } it 'does not enable runner without access to' do - post api("/projects/#{project.id}/runners", user), params: { runner_id: new_project_runner.id } + post api(path, user), params: { runner_id: new_project_runner.id } expect(response).to have_gitlab_http_status(:forbidden) end @@ -1284,7 +1320,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'authorized user without permissions' do it 'does not enable runner' do - post api("/projects/#{project.id}/runners", user2) + post api(path, user2) expect(response).to have_gitlab_http_status(:forbidden) end @@ -1292,7 +1328,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do context 'unauthorized user' do it 'does not enable runner' do - post api("/projects/#{project.id}/runners") + post api(path) expect(response).to have_gitlab_http_status(:unauthorized) end diff --git a/spec/requests/api/ci/secure_files_spec.rb b/spec/requests/api/ci/secure_files_spec.rb index fc988800b56..db12576154e 100644 --- a/spec/requests/api/ci/secure_files_spec.rb +++ b/spec/requests/api/ci/secure_files_spec.rb @@ -136,7 +136,7 @@ RSpec.describe API::Ci::SecureFiles, feature_category: :mobile_devops do expect(response).to have_gitlab_http_status(:ok) expect(json_response['name']).to eq(secure_file_with_metadata.name) - expect(json_response['expires_at']).to eq('2022-04-26T19:20:40.000Z') + expect(json_response['expires_at']).to eq('2023-04-26T19:20:39.000Z') expect(json_response['metadata'].keys).to match_array(%w[id issuer subject expires_at]) expect(json_response['file_extension']).to eq('cer') end diff --git a/spec/requests/api/ci/variables_spec.rb b/spec/requests/api/ci/variables_spec.rb index 0f9f1bc80d6..e937c4c2b8f 100644 --- a/spec/requests/api/ci/variables_spec.rb +++ b/spec/requests/api/ci/variables_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Ci::Variables, feature_category: :pipeline_authoring do +RSpec.describe API::Ci::Variables, feature_category: :secrets_management do let(:user) { create(:user) } let(:user2) { create(:user) } let!(:project) { create(:project, creator_id: user.id) } diff --git a/spec/requests/api/clusters/agent_tokens_spec.rb b/spec/requests/api/clusters/agent_tokens_spec.rb index b2d996e8002..2647684c9f8 100644 --- a/spec/requests/api/clusters/agent_tokens_spec.rb +++ b/spec/requests/api/clusters/agent_tokens_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Clusters::AgentTokens, feature_category: :kubernetes_management do +RSpec.describe API::Clusters::AgentTokens, feature_category: :deployment_management do let_it_be(:agent) { create(:cluster_agent) } let_it_be(:agent_token_one) { create(:cluster_agent_token, agent: agent) } let_it_be(:revoked_agent_token) { create(:cluster_agent_token, :revoked, agent: agent) } @@ -17,18 +17,16 @@ RSpec.describe API::Clusters::AgentTokens, feature_category: :kubernetes_managem describe 'GET /projects/:id/cluster_agents/:agent_id/tokens' do context 'with authorized user' do - it 'returns tokens regardless of status' do + it 'only returns active agent tokens' do get api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens", user) aggregate_failures "testing response" do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(response).to match_response_schema('public_api/v4/agent_tokens') - expect(json_response.count).to eq(2) + expect(json_response.count).to eq(1) expect(json_response.first['name']).to eq(agent_token_one.name) expect(json_response.first['agent_id']).to eq(agent.id) - expect(json_response.second['name']).to eq(revoked_agent_token.name) - expect(json_response.second['agent_id']).to eq(agent.id) end end @@ -80,17 +78,10 @@ RSpec.describe API::Clusters::AgentTokens, feature_category: :kubernetes_managem end end - it 'returns an agent token that is revoked' do + it 'returns a 404 if agent token is revoked' do get api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens/#{revoked_agent_token.id}", user) - aggregate_failures "testing response" do - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/agent_token') - expect(json_response['id']).to eq(revoked_agent_token.id) - expect(json_response['name']).to eq(revoked_agent_token.name) - expect(json_response['agent_id']).to eq(agent.id) - expect(json_response['status']).to eq('revoked') - end + expect(response).to have_gitlab_http_status(:not_found) end it 'returns a 404 if agent does not exist' do diff --git a/spec/requests/api/clusters/agents_spec.rb b/spec/requests/api/clusters/agents_spec.rb index a09713bd6e7..12056567e9d 100644 --- a/spec/requests/api/clusters/agents_spec.rb +++ b/spec/requests/api/clusters/agents_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Clusters::Agents, feature_category: :kubernetes_management do +RSpec.describe API::Clusters::Agents, feature_category: :deployment_management do let_it_be(:agent) { create(:cluster_agent) } let(:user) { agent.created_by_user } diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 025d065df7b..7540e19e278 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -533,8 +533,8 @@ RSpec.describe API::CommitStatuses, feature_category: :continuous_integration do end end - context 'with partitions' do - let(:current_partition_id) { 123 } + context 'with partitions', :ci_partitionable do + let(:current_partition_id) { ci_testing_partition_id } before do allow(Ci::Pipeline) diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index bcc27a80cf8..28126f1bdc2 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -132,6 +132,42 @@ RSpec.describe API::Commits, feature_category: :source_code_management do it_behaves_like 'project commits' end + context 'with author parameter' do + let(:params) { { author: 'Zaporozhets' } } + + it 'returns only this author commits' do + get api(route, user), params: params + + expect(response).to have_gitlab_http_status(:ok) + + author_names = json_response.map { |commit| commit['author_name'] }.uniq + + expect(author_names).to contain_exactly('Dmitriy Zaporozhets') + end + + context 'when author is missing' do + let(:params) { { author: '' } } + + it 'returns all commits' do + get api(route, user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq(20) + end + end + + context 'when author does not exists' do + let(:params) { { author: 'does not exist' } } + + it 'returns an empty list' do + get api(route, user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq([]) + end + end + end + context 'when repository does not exist' do let(:project) { create(:project, creator: user, path: 'my.project') } @@ -425,6 +461,27 @@ RSpec.describe API::Commits, feature_category: :source_code_management do describe "POST /projects/:id/repository/commits" do let!(:url) { "/projects/#{project_id}/repository/commits" } + context 'when unauthenticated', 'and project is public' do + let_it_be(:project) { create(:project, :public, :repository) } + let(:params) do + { + branch: 'master', + commit_message: 'message', + actions: [ + { + action: 'create', + file_path: '/test.rb', + content: 'puts 8' + } + ] + } + end + + it_behaves_like '401 response' do + let(:request) { post api(url), params: params } + end + end + it 'returns a 403 unauthorized for user without permissions' do post api(url, guest) @@ -523,7 +580,6 @@ RSpec.describe API::Commits, feature_category: :source_code_management do let(:property) { 'g_edit_by_web_ide' } let(:label) { 'usage_activity_by_stage_monthly.create.action_monthly_active_users_ide_edit' } let(:context) { [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event_name).to_context] } - let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } end context 'counts.web_ide_commits Snowplow event tracking' do @@ -536,7 +592,6 @@ RSpec.describe API::Commits, feature_category: :source_code_management do let(:category) { described_class.to_s } let(:namespace) { project.namespace.reload } let(:label) { 'counts.web_ide_commits' } - let(:feature_flag_name) { 'route_hll_to_snowplow_phase3' } let(:context) do [Gitlab::Tracking::ServicePingContext.new(data_source: :redis, key_path: 'counts.web_ide_commits').to_context.to_json] end @@ -1776,7 +1831,7 @@ RSpec.describe API::Commits, feature_category: :source_code_management do context 'when unauthenticated', 'and project is public' do let_it_be(:project) { create(:project, :public, :repository) } - it_behaves_like '403 response' do + it_behaves_like '401 response' do let(:request) { post api(route), params: { branch: 'master' } } end end @@ -1956,7 +2011,7 @@ RSpec.describe API::Commits, feature_category: :source_code_management do context 'when unauthenticated', 'and project is public' do let_it_be(:project) { create(:project, :public, :repository) } - it_behaves_like '403 response' do + it_behaves_like '401 response' do let(:request) { post api(route), params: { branch: branch } } end end diff --git a/spec/requests/api/composer_packages_spec.rb b/spec/requests/api/composer_packages_spec.rb index 0c726d46a01..2bb2ffa03c4 100644 --- a/spec/requests/api/composer_packages_spec.rb +++ b/spec/requests/api/composer_packages_spec.rb @@ -504,7 +504,11 @@ RSpec.describe API::ComposerPackages, feature_category: :package_registry do include_context 'Composer user type', params[:user_role], params[:member] do if params[:expected_status] == :success let(:snowplow_gitlab_standard_context) do - { project: project, namespace: project.namespace, property: 'i_package_composer_user' } + if user_role == :anonymous || (project_visibility_level == 'PUBLIC' && user_token == false) + { project: project, namespace: project.namespace, property: 'i_package_composer_user' } + else + { project: project, namespace: project.namespace, property: 'i_package_composer_user', user: user } + end end it_behaves_like 'a package tracking event', described_class.name, 'pull_package' diff --git a/spec/requests/api/conan_project_packages_spec.rb b/spec/requests/api/conan_project_packages_spec.rb index 814745f9e29..06f175233db 100644 --- a/spec/requests/api/conan_project_packages_spec.rb +++ b/spec/requests/api/conan_project_packages_spec.rb @@ -33,6 +33,29 @@ RSpec.describe API::ConanProjectPackages, feature_category: :package_registry do subject { get api(url), params: params } end + + context 'with access to package registry for everyone' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC) + + get api(url), params: params + end + + subject { json_response['results'] } + + context 'with a matching name' do + let(:params) { { q: package.conan_recipe } } + + it { is_expected.to contain_exactly(package.conan_recipe) } + end + + context 'with a * wildcard' do + let(:params) { { q: "#{package.name[0, 3]}*" } } + + it { is_expected.to contain_exactly(package.conan_recipe) } + end + end end describe 'GET /api/v4/projects/:id/packages/conan/v1/users/authenticate' do diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb index 0c80b7d830f..9c726e5a5f7 100644 --- a/spec/requests/api/debian_group_packages_spec.rb +++ b/spec/requests/api/debian_group_packages_spec.rb @@ -6,76 +6,119 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do include WorkhorseHelpers include_context 'Debian repository shared context', :group, false do + shared_examples 'a Debian package tracking event' do |action| + include_context 'Debian repository access', :public, :developer, :basic do + let(:snowplow_gitlab_standard_context) do + { project: nil, namespace: container, user: user, property: 'i_package_debian_user' } + end + + it_behaves_like 'a package tracking event', described_class.name, action + end + end + + shared_examples 'not a Debian package tracking event' do + include_context 'Debian repository access', :public, :developer, :basic do + it_behaves_like 'not a package tracking event', described_class.name, /.*/ + end + end + context 'with invalid parameter' do let(:url) { "/groups/1/-/packages/debian/dists/with+space/InRelease" } it_behaves_like 'Debian packages GET request', :bad_request, /^distribution is invalid$/ + it_behaves_like 'not a Debian package tracking event' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release.gpg' do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/Release.gpg" } it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNATURE-----/ + it_behaves_like 'not a Debian package tracking event' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release' do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/Release" } it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Codename: fixture-distribution\n$/ + it_behaves_like 'a Debian package tracking event', 'list_package' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/InRelease' do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/InRelease" } it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNED MESSAGE-----/ + it_behaves_like 'a Debian package tracking event', 'list_package' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do - let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages" } + let(:target_component_file) { component_file } + let(:target_component_name) { component.name } + let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/binary-#{architecture.name}/Packages" } - it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/ + it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete Packages file/ + it_behaves_like 'a Debian package tracking event', 'list_package' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages.gz' do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages.gz" } it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/ + it_behaves_like 'not a Debian package tracking event' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do - let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" } + let(:target_component_file) { component_file_older_sha256 } + let(:target_component_name) { component.name } + let(:target_sha256) { target_component_file.file_sha256 } + let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/binary-#{architecture.name}/by-hash/SHA256/#{target_sha256}" } - it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/ + it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/ + it_behaves_like 'a Debian package tracking event', 'list_package' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/source/Sources' do - let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/source/Sources" } + let(:target_component_file) { component_file_sources } + let(:target_component_name) { component.name } + let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/Sources" } - it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Sources file/ + it_behaves_like 'Debian packages index endpoint', /^Description: This is an incomplete Sources file$/ + it_behaves_like 'a Debian package tracking event', 'list_package' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/source/by-hash/SHA256/:file_sha256' do - let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/source/by-hash/SHA256/#{component_file_sources_older_sha256.file_sha256}" } + let(:target_component_file) { component_file_sources_older_sha256 } + let(:target_component_name) { component.name } + let(:target_sha256) { target_component_file.file_sha256 } + let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/by-hash/SHA256/#{target_sha256}" } - it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/ + it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/ + it_behaves_like 'a Debian package tracking event', 'list_package' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages' do - let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages" } + let(:target_component_file) { component_file_di } + let(:target_component_name) { component.name } + let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/debian-installer/binary-#{architecture.name}/Packages" } - it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete D-I Packages file/ + it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete D-I Packages file/ + it_behaves_like 'a Debian package tracking event', 'list_package' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages.gz' do let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages.gz" } it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/ + it_behaves_like 'not a Debian package tracking event' end describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do - let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" } + let(:target_component_file) { component_file_di_older_sha256 } + let(:target_component_name) { component.name } + let(:target_sha256) { target_component_file.file_sha256 } + let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{target_sha256}" } - it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/ + it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/ + it_behaves_like 'a Debian package tracking event', 'list_package' end describe 'GET groups/:id/-/packages/debian/pool/:codename/:project_id/:letter/:package_name/:package_version/:file_name' do @@ -89,12 +132,14 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do 'sample_1.2.3~alpha2.dsc' | /^Format: 3.0 \(native\)/ 'libsample0_1.2.3~alpha2_amd64.deb' | /^!<arch>/ 'sample-udeb_1.2.3~alpha2_amd64.udeb' | /^!<arch>/ + 'sample-ddeb_1.2.3~alpha2_amd64.ddeb' | /^!<arch>/ 'sample_1.2.3~alpha2_amd64.buildinfo' | /Build-Tainted-By/ 'sample_1.2.3~alpha2_amd64.changes' | /urgency=medium/ end with_them do it_behaves_like 'Debian packages read endpoint', 'GET', :success, params[:success_body] + it_behaves_like 'a Debian package tracking event', 'pull_package' context 'for bumping last downloaded at' do include_context 'Debian repository access', :public, :developer, :basic do diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb index 46f79efd928..030962044c6 100644 --- a/spec/requests/api/debian_project_packages_spec.rb +++ b/spec/requests/api/debian_project_packages_spec.rb @@ -6,6 +6,22 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d include WorkhorseHelpers include_context 'Debian repository shared context', :project, false do + shared_examples 'a Debian package tracking event' do |action| + include_context 'Debian repository access', :public, :developer, :basic do + let(:snowplow_gitlab_standard_context) do + { project: container, namespace: container.namespace, user: user, property: 'i_package_debian_user' } + end + + it_behaves_like 'a package tracking event', described_class.name, action + end + end + + shared_examples 'not a Debian package tracking event' do + include_context 'Debian repository access', :public, :developer, :basic do + it_behaves_like 'not a package tracking event', described_class.name, /.*/ + end + end + shared_examples 'accept GET request on private project with access to package registry for everyone' do include_context 'Debian repository access', :private, :anonymous, :basic do before do @@ -20,12 +36,14 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:url) { "/projects/1/packages/debian/dists/with+space/InRelease" } it_behaves_like 'Debian packages GET request', :bad_request, /^distribution is invalid$/ + it_behaves_like 'not a Debian package tracking event' end describe 'GET projects/:id/packages/debian/dists/*distribution/Release.gpg' do let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/Release.gpg" } it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNATURE-----/ + it_behaves_like 'not a Debian package tracking event' it_behaves_like 'accept GET request on private project with access to package registry for everyone' end @@ -33,6 +51,7 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/Release" } it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Codename: fixture-distribution\n$/ + it_behaves_like 'a Debian package tracking event', 'list_package' it_behaves_like 'accept GET request on private project with access to package registry for everyone' end @@ -40,13 +59,17 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/InRelease" } it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNED MESSAGE-----/ + it_behaves_like 'a Debian package tracking event', 'list_package' it_behaves_like 'accept GET request on private project with access to package registry for everyone' end describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do - let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages" } + let(:target_component_file) { component_file } + let(:target_component_name) { component.name } + let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/binary-#{architecture.name}/Packages" } - it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/ + it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete Packages file/ + it_behaves_like 'a Debian package tracking event', 'list_package' it_behaves_like 'accept GET request on private project with access to package registry for everyone' end @@ -54,33 +77,48 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages.gz" } it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/ + it_behaves_like 'not a Debian package tracking event' end describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do - let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" } + let(:target_component_file) { component_file_older_sha256 } + let(:target_component_name) { component.name } + let(:target_sha256) { target_component_file.file_sha256 } + let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/binary-#{architecture.name}/by-hash/SHA256/#{target_sha256}" } - it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/ + it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/ + it_behaves_like 'a Debian package tracking event', 'list_package' it_behaves_like 'accept GET request on private project with access to package registry for everyone' end describe 'GET projects/:id/packages/debian/dists/*distribution/:component/source/Sources' do - let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/source/Sources" } + let(:target_component_file) { component_file_sources } + let(:target_component_name) { component.name } + let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/Sources" } - it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Sources file/ + it_behaves_like 'Debian packages index endpoint', /^Description: This is an incomplete Sources file$/ + it_behaves_like 'a Debian package tracking event', 'list_package' it_behaves_like 'accept GET request on private project with access to package registry for everyone' end describe 'GET projects/:id/packages/debian/dists/*distribution/:component/source/by-hash/SHA256/:file_sha256' do - let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/source/by-hash/SHA256/#{component_file_sources_older_sha256.file_sha256}" } + let(:target_component_file) { component_file_sources_older_sha256 } + let(:target_component_name) { component.name } + let(:target_sha256) { target_component_file.file_sha256 } + let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/by-hash/SHA256/#{target_sha256}" } - it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/ + it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/ + it_behaves_like 'a Debian package tracking event', 'list_package' it_behaves_like 'accept GET request on private project with access to package registry for everyone' end describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages' do - let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages" } + let(:target_component_file) { component_file_di } + let(:target_component_name) { component.name } + let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/debian-installer/binary-#{architecture.name}/Packages" } - it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete D-I Packages file/ + it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete D-I Packages file/ + it_behaves_like 'a Debian package tracking event', 'list_package' it_behaves_like 'accept GET request on private project with access to package registry for everyone' end @@ -88,12 +126,17 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages.gz" } it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/ + it_behaves_like 'not a Debian package tracking event' end describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do - let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" } + let(:target_component_file) { component_file_di_older_sha256 } + let(:target_component_name) { component.name } + let(:target_sha256) { target_component_file.file_sha256 } + let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{target_sha256}" } - it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/ + it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/ + it_behaves_like 'a Debian package tracking event', 'list_package' it_behaves_like 'accept GET request on private project with access to package registry for everyone' end @@ -108,12 +151,14 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d 'sample_1.2.3~alpha2.dsc' | /^Format: 3.0 \(native\)/ 'libsample0_1.2.3~alpha2_amd64.deb' | /^!<arch>/ 'sample-udeb_1.2.3~alpha2_amd64.udeb' | /^!<arch>/ + 'sample-ddeb_1.2.3~alpha2_amd64.ddeb' | /^!<arch>/ 'sample_1.2.3~alpha2_amd64.buildinfo' | /Build-Tainted-By/ 'sample_1.2.3~alpha2_amd64.changes' | /urgency=medium/ end with_them do it_behaves_like 'Debian packages read endpoint', 'GET', :success, params[:success_body] + it_behaves_like 'a Debian package tracking event', 'pull_package' context 'for bumping last downloaded at' do include_context 'Debian repository access', :public, :developer, :basic do @@ -130,17 +175,19 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d describe 'PUT projects/:id/packages/debian/:file_name' do let(:method) { :put } let(:url) { "/projects/#{container.id}/packages/debian/#{file_name}" } - let(:snowplow_gitlab_standard_context) { { project: container, user: user, namespace: container.namespace } } context 'with a deb' do let(:file_name) { 'libsample0_1.2.3~alpha2_amd64.deb' } it_behaves_like 'Debian packages write endpoint', 'upload', :created, nil + it_behaves_like 'Debian packages endpoint catching ObjectStorage::RemoteStoreError' + it_behaves_like 'a Debian package tracking event', 'push_package' context 'with codename and component' do let(:extra_params) { { distribution: distribution.codename, component: 'main' } } it_behaves_like 'Debian packages write endpoint', 'upload', :created, nil + it_behaves_like 'a Debian package tracking event', 'push_package' end context 'with codename and without component' do @@ -149,6 +196,8 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d include_context 'Debian repository access', :public, :developer, :basic do it_behaves_like 'Debian packages GET request', :bad_request, /component is missing/ end + + it_behaves_like 'not a Debian package tracking event' end end @@ -157,13 +206,19 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d include_context 'Debian repository access', :public, :developer, :basic do it_behaves_like "Debian packages upload request", :created, nil + end - context 'with codename and component' do - let(:extra_params) { { distribution: distribution.codename, component: 'main' } } + it_behaves_like 'a Debian package tracking event', 'push_package' + context 'with codename and component' do + let(:extra_params) { { distribution: distribution.codename, component: 'main' } } + + include_context 'Debian repository access', :public, :developer, :basic do it_behaves_like "Debian packages upload request", :bad_request, - /^file_name Only debs and udebs can be directly added to a distribution$/ + /^file_name Only debs, udebs and ddebs can be directly added to a distribution$/ end + + it_behaves_like 'not a Debian package tracking event' end end @@ -171,6 +226,7 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:file_name) { 'sample_1.2.3~alpha2_amd64.changes' } it_behaves_like 'Debian packages write endpoint', 'upload', :created, nil + it_behaves_like 'a Debian package tracking event', 'push_package' end end @@ -180,6 +236,7 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d let(:url) { "/projects/#{container.id}/packages/debian/#{file_name}/authorize" } it_behaves_like 'Debian packages write endpoint', 'upload authorize', :created, nil + it_behaves_like 'not a Debian package tracking event' end end end diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 15880d920c5..18a9211df3e 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do +RSpec.describe API::DeployKeys, :aggregate_failures, feature_category: :continuous_delivery do let_it_be(:user) { create(:user) } let_it_be(:maintainer) { create(:user) } let_it_be(:admin) { create(:admin) } @@ -11,33 +11,29 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do let_it_be(:project3) { create(:project, creator_id: user.id) } let_it_be(:deploy_key) { create(:deploy_key, public: true) } let_it_be(:deploy_key_private) { create(:deploy_key, public: false) } + let_it_be(:path) { '/deploy_keys' } + let_it_be(:project_path) { "/projects/#{project.id}#{path}" } let!(:deploy_keys_project) do create(:deploy_keys_project, project: project, deploy_key: deploy_key) end describe 'GET /deploy_keys' do + it_behaves_like 'GET request permissions for admin mode' + context 'when unauthenticated' do it 'returns authentication error' do - get api('/deploy_keys') + get api(path) expect(response).to have_gitlab_http_status(:unauthorized) end end - context 'when authenticated as non-admin user' do - it 'returns a 403 error' do - get api('/deploy_keys', user) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - context 'when authenticated as admin' do - let_it_be(:pat) { create(:personal_access_token, user: admin) } + let_it_be(:pat) { create(:personal_access_token, :admin_mode, user: admin) } def make_api_request(params = {}) - get api('/deploy_keys', personal_access_token: pat), params: params + get api(path, personal_access_token: pat), params: params end it 'returns all deploy keys' do @@ -91,14 +87,18 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do describe 'GET /projects/:id/deploy_keys' do let(:deploy_key) { create(:deploy_key, public: true, user: admin) } + it_behaves_like 'GET request permissions for admin mode' do + let(:path) { project_path } + let(:failed_status_code) { :not_found } + end + def perform_request - get api("/projects/#{project.id}/deploy_keys", admin) + get api(project_path, admin, admin_mode: true) end it 'returns array of ssh keys' do perform_request - expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.first['title']).to eq(deploy_key.title) @@ -117,31 +117,59 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do end describe 'GET /projects/:id/deploy_keys/:key_id' do + let_it_be(:path) { "#{project_path}/#{deploy_key.id}" } + let_it_be(:unfindable_path) { "#{project_path}/404" } + + it_behaves_like 'GET request permissions for admin mode' do + let(:failed_status_code) { :not_found } + end + it 'returns a single key' do - get api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) + get api(path, admin, admin_mode: true) - expect(response).to have_gitlab_http_status(:ok) expect(json_response['title']).to eq(deploy_key.title) expect(json_response).not_to have_key(:projects_with_write_access) end it 'returns 404 Not Found with invalid ID' do - get api("/projects/#{project.id}/deploy_keys/404", admin) + get api(unfindable_path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end + + context 'when deploy key has expiry date' do + let(:deploy_key) { create(:deploy_key, :expired, public: true) } + let(:deploy_keys_project) { create(:deploy_keys_project, project: project, deploy_key: deploy_key) } + + it 'returns expiry date' do + get api("#{project_path}/#{deploy_key.id}", admin, admin_mode: true) + + expect(response).to have_gitlab_http_status(:ok) + expect(Time.parse(json_response['expires_at'])).to be_like_time(deploy_key.expires_at) + end + end end describe 'POST /projects/:id/deploy_keys' do + around do |example| + freeze_time { example.run } + end + + it_behaves_like 'POST request permissions for admin mode', :not_found do + let(:params) { attributes_for :another_key } + let(:path) { project_path } + let(:failed_status_code) { :not_found } + end + it 'does not create an invalid ssh key' do - post api("/projects/#{project.id}/deploy_keys", admin), params: { title: 'invalid key' } + post api(project_path, admin, admin_mode: true), params: { title: 'invalid key' } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq('key is missing') end it 'does not create a key without title' do - post api("/projects/#{project.id}/deploy_keys", admin), params: { key: 'some key' } + post api(project_path, admin, admin_mode: true), params: { key: 'some key' } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq('title is missing') @@ -151,7 +179,7 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do key_attrs = attributes_for :another_key expect do - post api("/projects/#{project.id}/deploy_keys", admin), params: key_attrs + post api(project_path, admin, admin_mode: true), params: key_attrs end.to change { project.deploy_keys.count }.by(1) new_key = project.deploy_keys.last @@ -161,7 +189,7 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do it 'returns an existing ssh key when attempting to add a duplicate' do expect do - post api("/projects/#{project.id}/deploy_keys", admin), params: { key: deploy_key.key, title: deploy_key.title } + post api(project_path, admin, admin_mode: true), params: { key: deploy_key.key, title: deploy_key.title } end.not_to change { project.deploy_keys.count } expect(response).to have_gitlab_http_status(:created) @@ -169,7 +197,7 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do it 'joins an existing ssh key to a new project' do expect do - post api("/projects/#{project2.id}/deploy_keys", admin), params: { key: deploy_key.key, title: deploy_key.title } + post api("/projects/#{project2.id}/deploy_keys", admin, admin_mode: true), params: { key: deploy_key.key, title: deploy_key.title } end.to change { project2.deploy_keys.count }.by(1) expect(response).to have_gitlab_http_status(:created) @@ -178,18 +206,34 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do it 'accepts can_push parameter' do key_attrs = attributes_for(:another_key).merge(can_push: true) - post api("/projects/#{project.id}/deploy_keys", admin), params: key_attrs + post api(project_path, admin, admin_mode: true), params: key_attrs expect(response).to have_gitlab_http_status(:created) expect(json_response['can_push']).to eq(true) end + + it 'accepts expires_at parameter' do + key_attrs = attributes_for(:another_key).merge(expires_at: 2.days.since.iso8601) + + post api(project_path, admin, admin_mode: true), params: key_attrs + + expect(response).to have_gitlab_http_status(:created) + expect(Time.parse(json_response['expires_at'])).to be_like_time(2.days.since) + end end describe 'PUT /projects/:id/deploy_keys/:key_id' do + let(:path) { "#{project_path}/#{deploy_key.id}" } let(:extra_params) { {} } + let(:admin_mode) { false } + + it_behaves_like 'PUT request permissions for admin mode' do + let(:params) { { title: 'new title', can_push: true } } + let(:failed_status_code) { :not_found } + end subject do - put api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", api_user), params: extra_params + put api(path, api_user, admin_mode: admin_mode), params: extra_params end context 'with non-admin' do @@ -204,6 +248,7 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do context 'with admin' do let(:api_user) { admin } + let(:admin_mode) { true } context 'public deploy key attached to project' do let(:extra_params) { { title: 'new title', can_push: true } } @@ -258,9 +303,13 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do context 'public deploy key attached to project' do let(:extra_params) { { title: 'new title', can_push: true } } - it 'updates the title of the deploy key' do - expect { subject }.to change { deploy_key.reload.title }.to 'new title' - expect(response).to have_gitlab_http_status(:ok) + context 'with admin mode on' do + let(:admin_mode) { true } + + it 'updates the title of the deploy key' do + expect { subject }.to change { deploy_key.reload.title }.to 'new title' + expect(response).to have_gitlab_http_status(:ok) + end end it 'updates can_push of deploy_keys_project' do @@ -298,18 +347,22 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do deploy_key end + let(:path) { "#{project_path}/#{deploy_key.id}" } + + it_behaves_like 'DELETE request permissions for admin mode' do + let(:failed_status_code) { :not_found } + end + it 'removes existing key from project' do expect do - delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) - - expect(response).to have_gitlab_http_status(:no_content) + delete api(path, admin, admin_mode: true) end.to change { project.deploy_keys.count }.by(-1) end context 'when the deploy key is public' do it 'does not delete the deploy key' do expect do - delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) + delete api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:no_content) end.not_to change { DeployKey.count } @@ -322,7 +375,7 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do context 'when the deploy key is only used by this project' do it 'deletes the deploy key' do expect do - delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) + delete api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:no_content) end.to change { DeployKey.count }.by(-1) @@ -336,7 +389,7 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do it 'does not delete the deploy key' do expect do - delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) + delete api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:no_content) end.not_to change { DeployKey.count } @@ -345,26 +398,31 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do end it 'returns 404 Not Found with invalid ID' do - delete api("/projects/#{project.id}/deploy_keys/404", admin) + delete api("#{project_path}/404", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end it_behaves_like '412 response' do - let(:request) { api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) } + let(:request) { api("#{project_path}/#{deploy_key.id}", admin, admin_mode: true) } end end describe 'POST /projects/:id/deploy_keys/:key_id/enable' do - let(:project2) { create(:project) } + let_it_be(:project2) { create(:project) } + let_it_be(:path) { "/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable" } + let_it_be(:params) { {} } + + it_behaves_like 'POST request permissions for admin mode' do + let(:failed_status_code) { :not_found } + end context 'when the user can admin the project' do it 'enables the key' do expect do - post api("/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable", admin) + post api(path, admin, admin_mode: true) end.to change { project2.deploy_keys.count }.from(0).to(1) - expect(response).to have_gitlab_http_status(:created) expect(json_response['id']).to eq(deploy_key.id) end end diff --git a/spec/requests/api/deploy_tokens_spec.rb b/spec/requests/api/deploy_tokens_spec.rb index 4efe49e843f..c0e36bf03bf 100644 --- a/spec/requests/api/deploy_tokens_spec.rb +++ b/spec/requests/api/deploy_tokens_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do +RSpec.describe API::DeployTokens, :aggregate_failures, feature_category: :continuous_delivery do let_it_be(:user) { create(:user) } let_it_be(:creator) { create(:user) } let_it_be(:project) { create(:project, creator_id: creator.id) } @@ -17,26 +17,25 @@ RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do describe 'GET /deploy_tokens' do subject do - get api('/deploy_tokens', user) + get api('/deploy_tokens', user, admin_mode: admin_mode) response end - context 'when unauthenticated' do - let(:user) { nil } + let_it_be(:admin_mode) { false } - it { is_expected.to have_gitlab_http_status(:unauthorized) } + it_behaves_like 'GET request permissions for admin mode' do + let(:path) { '/deploy_tokens' } end - context 'when authenticated as non-admin user' do - let(:user) { creator } + context 'when unauthenticated' do + let(:user) { nil } - it { is_expected.to have_gitlab_http_status(:forbidden) } + it { is_expected.to have_gitlab_http_status(:unauthorized) } end context 'when authenticated as admin' do let(:user) { create(:admin) } - - it { is_expected.to have_gitlab_http_status(:ok) } + let_it_be(:admin_mode) { true } it 'returns all deploy tokens' do subject @@ -57,7 +56,7 @@ RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do context 'and active=true' do it 'only returns active deploy tokens' do - get api('/deploy_tokens?active=true', user) + get api('/deploy_tokens?active=true', user, admin_mode: true) token_ids = json_response.map { |token| token['id'] } expect(response).to have_gitlab_http_status(:ok) @@ -73,8 +72,10 @@ RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do end describe 'GET /projects/:id/deploy_tokens' do + let(:path) { "/projects/#{project.id}/deploy_tokens" } + subject do - get api("/projects/#{project.id}/deploy_tokens", user) + get api(path, user) response end @@ -134,8 +135,10 @@ RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do end describe 'GET /projects/:id/deploy_tokens/:token_id' do + let(:path) { "/projects/#{project.id}/deploy_tokens/#{deploy_token.id}" } + subject do - get api("/projects/#{project.id}/deploy_tokens/#{deploy_token.id}", user) + get api(path, user) response end @@ -183,8 +186,10 @@ RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do end describe 'GET /groups/:id/deploy_tokens' do + let(:path) { "/groups/#{group.id}/deploy_tokens" } + subject do - get api("/groups/#{group.id}/deploy_tokens", user) + get api(path, user) response end @@ -241,8 +246,10 @@ RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do end describe 'GET /groups/:id/deploy_tokens/:token_id' do + let(:path) { "/groups/#{group.id}/deploy_tokens/#{group_deploy_token.id}" } + subject do - get api("/groups/#{group.id}/deploy_tokens/#{group_deploy_token.id}", user) + get api(path, user) response end @@ -290,8 +297,10 @@ RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do end describe 'DELETE /projects/:id/deploy_tokens/:token_id' do + let(:path) { "/projects/#{project.id}/deploy_tokens/#{deploy_token.id}" } + subject do - delete api("/projects/#{project.id}/deploy_tokens/#{deploy_token.id}", user) + delete api(path, user) response end @@ -455,8 +464,10 @@ RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do end describe 'DELETE /groups/:id/deploy_tokens/:token_id' do + let(:path) { "/groups/#{group.id}/deploy_tokens/#{group_deploy_token.id}" } + subject do - delete api("/groups/#{group.id}/deploy_tokens/#{group_deploy_token.id}", user) + delete api(path, user) response end diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index efe76c9cfda..3ca54cd40d0 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -47,11 +47,15 @@ RSpec.describe API::Deployments, feature_category: :continuous_delivery do end context 'when forbidden order_by is specified' do + before do + stub_feature_flags(deployments_raise_updated_at_inefficient_error_override: false) + end + it 'returns an error' do perform_request({ updated_before: 30.minutes.ago, updated_after: 90.minutes.ago, order_by: :id }) expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to include('`updated_at` filter and `updated_at` sorting must be paired') + expect(json_response['message']).to include('`updated_at` filter requires `updated_at` sort') end end end diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb index 5116f074894..888220c2251 100644 --- a/spec/requests/api/doorkeeper_access_spec.rb +++ b/spec/requests/api/doorkeeper_access_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'doorkeeper access', feature_category: :authentication_and_authorization do +RSpec.describe 'doorkeeper access', feature_category: :system_access do let!(:user) { create(:user) } let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) } let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" } diff --git a/spec/requests/api/draft_notes_spec.rb b/spec/requests/api/draft_notes_spec.rb index e8f519e004d..3911bb8bc00 100644 --- a/spec/requests/api/draft_notes_spec.rb +++ b/spec/requests/api/draft_notes_spec.rb @@ -8,11 +8,16 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do let_it_be(:project) { create(:project, :public) } let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) } + let_it_be(:private_project) { create(:project, :private) } + let_it_be(:private_merge_request) do + create(:merge_request, source_project: private_project, target_project: private_project) + end + let_it_be(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) } let!(:draft_note_by_current_user) { create(:draft_note, merge_request: merge_request, author: user) } let!(:draft_note_by_random_user) { create(:draft_note, merge_request: merge_request) } - let_it_be(:api_stub) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}" } + let_it_be(:base_url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes" } before do project.add_developer(user) @@ -20,13 +25,13 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do describe "Get a list of merge request draft notes" do it "returns 200 OK status" do - get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes", user) + get api(base_url, user) expect(response).to have_gitlab_http_status(:ok) end it "returns only draft notes authored by the current user" do - get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes", user) + get api(base_url, user) draft_note_ids = json_response.pluck("id") @@ -40,7 +45,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do context "when requesting an existing draft note by the user" do before do get api( - "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{draft_note_by_current_user.id}", + "#{base_url}/#{draft_note_by_current_user.id}", user ) end @@ -56,7 +61,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do context "when requesting a non-existent draft note" do it "returns a 404 Not Found response" do get api( - "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{DraftNote.last.id + 1}", + "#{base_url}/#{DraftNote.last.id + 1}", user ) @@ -67,7 +72,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do context "when requesting an existing draft note by another user" do it "returns a 404 Not Found response" do get api( - "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{draft_note_by_random_user.id}", + "#{base_url}/#{draft_note_by_random_user.id}", user ) @@ -83,7 +88,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do before do delete api( - "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{draft_note_by_current_user.id}", + "#{base_url}/#{draft_note_by_current_user.id}", user ) end @@ -100,7 +105,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do context "when deleting a non-existent draft note" do it "returns a 404 Not Found" do delete api( - "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{non_existing_record_id}", + "#{base_url}/#{non_existing_record_id}", user ) @@ -111,7 +116,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do context "when deleting a draft note by a different user" do it "returns a 404 Not Found" do delete api( - "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{draft_note_by_random_user.id}", + "#{base_url}/#{draft_note_by_random_user.id}", user ) @@ -120,10 +125,152 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do end end + def create_draft_note(params = {}, url = base_url) + post api(url, user), params: params + end + + describe "Create a new draft note" do + let(:basic_create_params) do + { + note: "Example body string" + } + end + + context "when creating a new draft note" do + context "with required params" do + it "returns 201 Created status" do + create_draft_note(basic_create_params) + + expect(response).to have_gitlab_http_status(:created) + end + + it "creates a new draft note with the submitted params" do + expect { create_draft_note(basic_create_params) }.to change { DraftNote.count }.by(1) + + expect(json_response["note"]).to eq(basic_create_params[:note]) + expect(json_response["merge_request_id"]).to eq(merge_request.id) + expect(json_response["author_id"]).to eq(user.id) + end + end + + context "without required params" do + it "returns 400 Bad Request status" do + create_draft_note({}) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context "when providing a non-existing commit_id" do + it "returns a 400 Bad Request" do + create_draft_note( + basic_create_params.merge( + commit_id: 'bad SHA' + ) + ) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context "when targeting a merge request the user doesn't have access to" do + it "returns a 404 Not Found" do + create_draft_note( + basic_create_params, + "/projects/#{private_project.id}/merge_requests/#{private_merge_request.iid}" + ) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context "when attempting to resolve a disscussion" do + context "when providing a non-existant ID" do + it "returns a 400 Bad Request" do + create_draft_note( + basic_create_params.merge( + resolve_discussion: true, + in_reply_to_discussion_id: non_existing_record_id + ) + ) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context "when not providing an ID" do + it "returns a 400 Bad Request" do + create_draft_note(basic_create_params.merge(resolve_discussion: true)) + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it "returns a validation error message" do + create_draft_note(basic_create_params.merge(resolve_discussion: true)) + + expect(response.body) + .to eq("{\"message\":{\"base\":[\"User is not allowed to resolve thread\"]}}") + end + end + end + end + end + + def update_draft_note(params = {}, url = base_url) + put api("#{url}/#{draft_note_by_current_user.id}", user), params: params + end + + describe "Update a draft note" do + let(:basic_update_params) do + { + note: "Example updated body string" + } + end + + context "when updating an existing draft note" do + context "with required params" do + it "returns 200 Success status" do + update_draft_note(basic_update_params) + + expect(response).to have_gitlab_http_status(:success) + end + + it "updates draft note with the new content" do + update_draft_note(basic_update_params) + + expect(json_response["note"]).to eq(basic_update_params[:note]) + end + end + + context "without including an update to the note body" do + it "returns the draft note with no changes" do + expect { update_draft_note({}) } + .not_to change { draft_note_by_current_user.note } + end + end + + context "when updating a non-existent draft note" do + it "returns a 404 Not Found" do + put api("#{base_url}/#{non_existing_record_id}", user), params: basic_update_params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context "when updating a draft note by a different user" do + it "returns a 404 Not Found" do + put api("#{base_url}/#{draft_note_by_random_user.id}", user), params: basic_update_params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end + describe "Publishing a draft note" do let(:publish_draft_note) do put api( - "#{api_stub}/draft_notes/#{draft_note_by_current_user.id}/publish", + "#{base_url}/#{draft_note_by_current_user.id}/publish", user ) end @@ -144,7 +291,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do context "when publishing a non-existent draft note" do it "returns a 404 Not Found" do put api( - "#{api_stub}/draft_notes/#{non_existing_record_id}/publish", + "#{base_url}/#{non_existing_record_id}/publish", user ) @@ -155,7 +302,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do context "when publishing a draft note by a different user" do it "returns a 404 Not Found" do put api( - "#{api_stub}/draft_notes/#{draft_note_by_random_user.id}/publish", + "#{base_url}/#{draft_note_by_random_user.id}/publish", user ) @@ -175,4 +322,47 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do end end end + + describe "Bulk publishing draft notes" do + let(:bulk_publish_draft_notes) do + post api( + "#{base_url}/bulk_publish", + user + ) + end + + let!(:draft_note_by_current_user_2) { create(:draft_note, merge_request: merge_request, author: user) } + + context "when publishing an existing draft note by the user" do + it "returns 204 No Content status" do + bulk_publish_draft_notes + + expect(response).to have_gitlab_http_status(:no_content) + end + + it "publishes the specified draft notes" do + expect { bulk_publish_draft_notes }.to change { Note.count }.by(2) + expect(DraftNote.exists?(draft_note_by_current_user.id)).to eq(false) + expect(DraftNote.exists?(draft_note_by_current_user_2.id)).to eq(false) + end + + it "only publishes the user's draft notes" do + bulk_publish_draft_notes + + expect(DraftNote.exists?(draft_note_by_random_user.id)).to eq(true) + end + end + + context "when DraftNotes::PublishService returns a non-success" do + it "returns an :internal_server_error and a message" do + expect_next_instance_of(DraftNotes::PublishService) do |instance| + expect(instance).to receive(:execute).and_return({ status: :failure, message: "Error message" }) + end + + bulk_publish_draft_notes + + expect(response).to have_gitlab_http_status(:internal_server_error) + end + end + end end diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 6164555ad19..9a435b3bce9 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -72,30 +72,11 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do end context "when params[:search] is less than #{described_class::MIN_SEARCH_LENGTH} characters" do - before do - stub_feature_flags(environment_search_api_min_chars: false) - end - - it 'returns a normal response' do + it 'returns with status 400' do get api("/projects/#{project.id}/environments?search=ab", user) - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.size).to eq(0) - end - - context 'and environment_search_api_min_chars flag is enabled for the project' do - before do - stub_feature_flags(environment_search_api_min_chars: project) - end - - it 'returns with status 400' do - get api("/projects/#{project.id}/environments?search=ab", user) - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to include("Search query is less than #{described_class::MIN_SEARCH_LENGTH} characters") - end + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to include("Search query is less than #{described_class::MIN_SEARCH_LENGTH} characters") end end @@ -229,14 +210,13 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do end describe 'PUT /projects/:id/environments/:environment_id' do - it 'returns a 200 if name and external_url are changed' do + it 'returns a 200 if external_url is changed' do url = 'https://mepmep.whatever.ninja' put api("/projects/#{project.id}/environments/#{environment.id}", user), - params: { name: 'Mepmep', external_url: url } + params: { 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 @@ -258,16 +238,6 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed") end - it "won't update the external_url if only the name is passed" do - url = environment.external_url - put api("/projects/#{project.id}/environments/#{environment.id}", user), - params: { name: 'Mepmep' } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['name']).to eq('Mepmep') - expect(json_response['external_url']).to eq(url) - end - it 'returns a 404 if the environment does not exist' do put api("/projects/#{project.id}/environments/#{non_existing_record_id}", user) diff --git a/spec/requests/api/error_tracking/project_settings_spec.rb b/spec/requests/api/error_tracking/project_settings_spec.rb index 5906cdf105a..bde90627983 100644 --- a/spec/requests/api/error_tracking/project_settings_spec.rb +++ b/spec/requests/api/error_tracking/project_settings_spec.rb @@ -4,9 +4,9 @@ require 'spec_helper' RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tracking do let_it_be(:user) { create(:user) } - - let(:setting) { create(:project_error_tracking_setting) } - let(:project) { setting.project } + let_it_be(:project) { create(:project) } + let_it_be(:setting) { create(:project_error_tracking_setting, project: project) } + let_it_be(:project_without_setting) { create(:project) } shared_examples 'returns project settings' do it 'returns correct project settings' do @@ -38,7 +38,7 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra end end - shared_examples 'returns 404' do + shared_examples 'returns no project settings' do it 'returns no project settings' do make_request @@ -48,8 +48,60 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra end end + shared_examples 'returns 400' do + it 'rejects request' do + make_request + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + shared_examples 'returns 401' do + it 'rejects request' do + make_request + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + shared_examples 'returns 403' do + it 'rejects request' do + make_request + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + shared_examples 'returns 404' do + it 'rejects request' do + make_request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + shared_examples 'returns 400 with `integrated` param required or invalid' do |error| + it 'returns 400' do + make_request + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']) + .to eq(error) + end + end + + shared_examples "returns error from UpdateService" do + it "returns errors" do + make_request + + expect(json_response['http_status']).to eq('forbidden') + expect(json_response['message']).to eq('An error occurred') + end + end + describe "PATCH /projects/:id/error_tracking/settings" do - let(:params) { { active: false } } + let(:params) { { active: false, integrated: integrated } } + let(:integrated) { false } def make_request patch api("/projects/#{project.id}/error_tracking/settings", user), params: params @@ -60,95 +112,97 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra project.add_maintainer(user) end - context 'patch settings' do - context 'integrated_error_tracking feature enabled' do - it_behaves_like 'returns project settings' - end - - context 'integrated_error_tracking feature disabled' do - before do - stub_feature_flags(integrated_error_tracking: false) - end + context 'with integrated_error_tracking feature enabled' do + it_behaves_like 'returns project settings' + end - it_behaves_like 'returns project settings with false for integrated' + context 'with integrated_error_tracking feature disabled' do + before do + stub_feature_flags(integrated_error_tracking: false) end - it 'updates enabled flag' do - expect(setting).to be_enabled + it_behaves_like 'returns project settings with false for integrated' + end - make_request + it 'updates enabled flag' do + expect(setting).to be_enabled - expect(json_response).to include('active' => false) - expect(setting.reload).not_to be_enabled - end + make_request + + expect(json_response).to include('active' => false) + expect(setting.reload).not_to be_enabled + end - context 'active is invalid' do - let(:params) { { active: "randomstring" } } + context 'when active is invalid' do + let(:params) { { active: "randomstring" } } - it 'returns active is invalid if non boolean' do - make_request + it 'returns active is invalid if non boolean' do + make_request - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']) - .to eq('active is invalid') - end + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']) + .to eq('active is invalid') end + end - context 'active is empty' do - let(:params) { { active: '' } } + context 'when active is empty' do + let(:params) { { active: '' } } - it 'returns 400' do - make_request + it 'returns 400' do + make_request - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']) - .to eq('active is empty') - end + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']) + .to eq('active is empty') end + end - context 'with integrated param' do - let(:params) { { active: true, integrated: true } } + context 'with integrated param' do + let(:params) { { active: true, integrated: true } } - context 'integrated_error_tracking feature enabled' do - before do - stub_feature_flags(integrated_error_tracking: true) - end + context 'when integrated_error_tracking feature enabled' do + before do + stub_feature_flags(integrated_error_tracking: true) + end - it 'updates the integrated flag' do - expect(setting.integrated).to be_falsey + it 'updates the integrated flag' do + expect(setting.integrated).to be_falsey - make_request + make_request - expect(json_response).to include('integrated' => true) - expect(setting.reload.integrated).to be_truthy - end + expect(json_response).to include('integrated' => true) + expect(setting.reload.integrated).to be_truthy end end end context 'without a project setting' do - let(:project) { create(:project) } + let(:project) { project_without_setting } before do project.add_maintainer(user) end - context 'patch settings' do - it_behaves_like 'returns 404' - end + it_behaves_like 'returns no project settings' end - end - context 'when authenticated as reporter' do - before do - project.add_reporter(user) - end + context "when ::Projects::Operations::UpdateService responds with an error" do + before do + allow_next_instance_of(::Projects::Operations::UpdateService) do |service| + allow(service) + .to receive(:execute) + .and_return({ status: :error, message: 'An error occurred', http_status: :forbidden }) + end + end - context 'patch request' do - it 'returns 403' do - make_request + context "when integrated" do + let(:integrated) { true } - expect(response).to have_gitlab_http_status(:forbidden) + it_behaves_like 'returns error from UpdateService' + end + + context "without integrated" do + it_behaves_like 'returns error from UpdateService' end end end @@ -158,35 +212,17 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra project.add_developer(user) end - context 'patch request' do - it 'returns 403' do - make_request - - expect(response).to have_gitlab_http_status(:forbidden) - end - end + it_behaves_like 'returns 403' end context 'when authenticated as non-member' do - context 'patch request' do - it 'returns 404' do - make_request - - expect(response).to have_gitlab_http_status(:not_found) - end - end + it_behaves_like 'returns 404' end context 'when unauthenticated' do let(:user) { nil } - context 'patch request' do - it 'returns 401 for update request' do - make_request - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end + it_behaves_like 'returns 401' end end @@ -200,77 +236,152 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra project.add_maintainer(user) end - context 'get settings' do - context 'integrated_error_tracking feature enabled' do - before do - stub_feature_flags(integrated_error_tracking: true) - end + it_behaves_like 'returns project settings' - it_behaves_like 'returns project settings' + context 'when integrated_error_tracking feature disabled' do + before do + stub_feature_flags(integrated_error_tracking: false) end - context 'integrated_error_tracking feature disabled' do - before do - stub_feature_flags(integrated_error_tracking: false) - end - - it_behaves_like 'returns project settings with false for integrated' - end + it_behaves_like 'returns project settings with false for integrated' end end context 'without a project setting' do - let(:project) { create(:project) } + let(:project) { project_without_setting } before do project.add_maintainer(user) end - context 'get settings' do - it_behaves_like 'returns 404' - end + it_behaves_like 'returns no project settings' end - context 'when authenticated as reporter' do + context 'when authenticated as developer' do before do - project.add_reporter(user) + project.add_developer(user) end - it 'returns 403' do - make_request + it_behaves_like 'returns 403' + end - expect(response).to have_gitlab_http_status(:forbidden) - end + context 'when authenticated as non-member' do + it_behaves_like 'returns 404' end - context 'when authenticated as developer' do - before do - project.add_developer(user) - end + context 'when unauthenticated' do + let(:user) { nil } - it 'returns 403' do - make_request + it_behaves_like 'returns 401' + end + end - expect(response).to have_gitlab_http_status(:forbidden) - end + describe "PUT /projects/:id/error_tracking/settings" do + let(:params) { { active: active, integrated: integrated } } + let(:active) { true } + let(:integrated) { true } + + def make_request + put api("/projects/#{project.id}/error_tracking/settings", user), params: params end - context 'when authenticated as non-member' do - it 'returns 404' do - make_request + context 'when authenticated' do + context 'as maintainer' do + before do + project.add_maintainer(user) + end + + context "when integrated" do + context "with existing setting" do + let(:project) { setting.project } + let(:setting) { create(:project_error_tracking_setting, :integrated) } + let(:active) { false } + + it "updates a setting" do + expect { make_request }.not_to change { ErrorTracking::ProjectErrorTrackingSetting.count } - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(:ok) + + expect(json_response).to eq( + "active" => false, + "api_url" => nil, + "integrated" => integrated, + "project_name" => nil, + "sentry_external_url" => nil + ) + end + end + + context "without setting" do + let(:project) { project_without_setting } + let(:active) { true } + + it "creates a setting" do + expect { make_request }.to change { ErrorTracking::ProjectErrorTrackingSetting.count } + + expect(response).to have_gitlab_http_status(:ok) + + expect(json_response).to eq( + "active" => true, + "api_url" => nil, + "integrated" => true, + "project_name" => nil, + "sentry_external_url" => nil + ) + end + end + + context "when ::Projects::Operations::UpdateService responds with an error" do + before do + allow_next_instance_of(::Projects::Operations::UpdateService) do |service| + allow(service) + .to receive(:execute) + .and_return({ status: :error, message: 'An error occurred', http_status: :forbidden }) + end + end + + it_behaves_like 'returns error from UpdateService' + end + end + + context "when integrated_error_tracking feature disabled" do + before do + stub_feature_flags(integrated_error_tracking: false) + end + + it_behaves_like 'returns 404' + end + + context "when integrated param is invalid" do + let(:params) { { active: active, integrated: 'invalid_string' } } + + it_behaves_like 'returns 400 with `integrated` param required or invalid', 'integrated is invalid' + end + + context "when integrated param is missing" do + let(:params) { { active: active } } + + it_behaves_like 'returns 400 with `integrated` param required or invalid', 'integrated is missing' + end end - end - context 'when unauthenticated' do - let(:user) { nil } + context "as developer" do + before do + project.add_developer(user) + end - it 'returns 401' do - make_request + it_behaves_like 'returns 403' + end - expect(response).to have_gitlab_http_status(:unauthorized) + context 'as non-member' do + it_behaves_like 'returns 404' end end + + context "when unauthorized" do + let(:user) { nil } + + it_behaves_like 'returns 401' + end end end diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index f4066c54c47..ed84e3e5f48 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -55,6 +55,11 @@ RSpec.describe API::Files, feature_category: :source_code_management do } end + let(:last_commit_for_path) do + Gitlab::Git::Commit + .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path)) + end + shared_context 'with author parameters' do let(:author_email) { 'user@example.org' } let(:author_name) { 'John Doe' } @@ -136,6 +141,12 @@ RSpec.describe API::Files, feature_category: :source_code_management do it 'caches sha256 of the content', :use_clean_rails_redis_caching do head api(route(file_path), current_user, **options), params: params + expect(Gitlab::Cache::Client).to receive(:build_with_metadata).with( + cache_identifier: 'API::Files#content_sha', + feature_category: :source_code_management, + backing_resource: :gitaly + ).and_call_original + expect(Rails.cache.fetch("blob_content_sha256:#{project.full_path}:#{response.headers['X-Gitlab-Blob-Id']}")) .to eq(content_sha256) @@ -829,7 +840,6 @@ RSpec.describe API::Files, feature_category: :source_code_management do expect_to_send_git_blob(api(url, current_user), params) expect(response.headers['Cache-Control']).to eq('max-age=0, private, must-revalidate, no-store, no-cache') - expect(response.headers['Pragma']).to eq('no-cache') expect(response.headers['Expires']).to eq('Fri, 01 Jan 1990 00:00:00 GMT') end @@ -1180,7 +1190,7 @@ RSpec.describe API::Files, feature_category: :source_code_management do end context 'when updating an existing file with stale last commit id' do - let(:params_with_stale_id) { params.merge(last_commit_id: 'stale') } + let(:params_with_stale_id) { params.merge(last_commit_id: last_commit_for_path.parent_id) } it 'returns a 400 bad request' do put api(route(file_path), user), params: params_with_stale_id @@ -1191,12 +1201,7 @@ RSpec.describe API::Files, feature_category: :source_code_management do end context 'with correct last commit id' do - let(:last_commit) do - Gitlab::Git::Commit - .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path)) - end - - let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) } + let(:params_with_correct_id) { params.merge(last_commit_id: last_commit_for_path.id) } it 'updates existing file in project repo' do put api(route(file_path), user), params: params_with_correct_id @@ -1206,12 +1211,7 @@ RSpec.describe API::Files, feature_category: :source_code_management do end context 'when file path is invalid' do - let(:last_commit) do - Gitlab::Git::Commit - .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path)) - end - - let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) } + let(:params_with_correct_id) { params.merge(last_commit_id: last_commit_for_path.id) } it 'returns a 400 bad request' do put api(route(invalid_file_path), user), params: params_with_correct_id @@ -1222,12 +1222,7 @@ RSpec.describe API::Files, feature_category: :source_code_management do end it_behaves_like 'when path is absolute' do - let(:last_commit) do - Gitlab::Git::Commit - .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path)) - end - - let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) } + let(:params_with_correct_id) { params.merge(last_commit_id: last_commit_for_path.id) } subject { put api(route(absolute_path), user), params: params_with_correct_id } end diff --git a/spec/requests/api/freeze_periods_spec.rb b/spec/requests/api/freeze_periods_spec.rb index 170871706dc..b582c2e0f4e 100644 --- a/spec/requests/api/freeze_periods_spec.rb +++ b/spec/requests/api/freeze_periods_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do +RSpec.describe API::FreezePeriods, :aggregate_failures, feature_category: :continuous_delivery do let_it_be(:project) { create(:project, :repository, :private) } let_it_be(:user) { create(:user) } let_it_be(:admin) { create(:admin) } @@ -12,11 +12,18 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do let(:last_freeze_period) { project.freeze_periods.last } describe 'GET /projects/:id/freeze_periods' do + let(:path) { "/projects/#{project.id}/freeze_periods" } + + it_behaves_like 'GET request permissions for admin mode' do + let!(:freeze_period) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) } + let(:failed_status_code) { :not_found } + end + context 'when the user is the admin' do let!(:freeze_period) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) } it 'returns 200 HTTP status' do - get api("/projects/#{project.id}/freeze_periods", admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) end @@ -32,20 +39,20 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do let!(:freeze_period_2) { create(:ci_freeze_period, project: project, created_at: 1.day.ago) } it 'returns 200 HTTP status' do - get api("/projects/#{project.id}/freeze_periods", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) end it 'returns freeze_periods ordered by created_at ascending' do - get api("/projects/#{project.id}/freeze_periods", user) + get api(path, user) expect(json_response.count).to eq(2) expect(freeze_period_ids).to eq([freeze_period_1.id, freeze_period_2.id]) end it 'matches response schema' do - get api("/projects/#{project.id}/freeze_periods", user) + get api(path, user) expect(response).to match_response_schema('public_api/v4/freeze_periods') end @@ -53,13 +60,13 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do context 'when there are no freeze_periods' do it 'returns 200 HTTP status' do - get api("/projects/#{project.id}/freeze_periods", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) end it 'returns an empty response' do - get api("/projects/#{project.id}/freeze_periods", user) + get api(path, user) expect(json_response).to be_empty end @@ -76,7 +83,7 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do end it 'responds 403 Forbidden' do - get api("/projects/#{project.id}/freeze_periods", user) + get api(path, user) expect(response).to have_gitlab_http_status(:forbidden) end @@ -84,7 +91,7 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do context 'when user is not a project member' do it 'responds 404 Not Found' do - get api("/projects/#{project.id}/freeze_periods", user) + get api(path, user) expect(response).to have_gitlab_http_status(:not_found) end @@ -93,7 +100,7 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do let(:project) { create(:project, :public) } it 'responds 403 Forbidden' do - get api("/projects/#{project.id}/freeze_periods", user) + get api(path, user) expect(response).to have_gitlab_http_status(:forbidden) end @@ -102,6 +109,16 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do end describe 'GET /projects/:id/freeze_periods/:freeze_period_id' do + let(:path) { "/projects/#{project.id}/freeze_periods/#{freeze_period.id}" } + + it_behaves_like 'GET request permissions for admin mode' do + let!(:freeze_period) do + create(:ci_freeze_period, project: project) + end + + let(:failed_status_code) { :not_found } + end + context 'when there is a freeze period' do let!(:freeze_period) do create(:ci_freeze_period, project: project) @@ -111,7 +128,7 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do let!(:freeze_period) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) } it 'responds 200 OK' do - get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) end @@ -123,13 +140,13 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do end it 'responds 200 OK' do - get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) end it 'returns a freeze period' do - get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user) + get api(path, user) expect(json_response).to include( 'id' => freeze_period.id, @@ -139,7 +156,7 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do end it 'matches response schema' do - get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user) + get api(path, user) expect(response).to match_response_schema('public_api/v4/freeze_period') end @@ -151,7 +168,7 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do end it 'responds 403 Forbidden' do - get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:forbidden) end @@ -161,7 +178,7 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do context 'when freeze_period exists' do it 'responds 403 Forbidden' do - get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:forbidden) end @@ -188,7 +205,15 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do } end - subject { post api("/projects/#{project.id}/freeze_periods", api_user), params: params } + let(:path) { "/projects/#{project.id}/freeze_periods" } + + it_behaves_like 'POST request permissions for admin mode' do + let(:failed_status_code) { :not_found } + end + + subject do + post api(path, api_user, admin_mode: api_user.admin?), params: params + end context 'when the user is the admin' do let(:api_user) { admin } @@ -310,7 +335,10 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do let(:params) { { freeze_start: '0 22 * * 5', freeze_end: '5 4 * * sun' } } let!(:freeze_period) { create :ci_freeze_period, project: project } - subject { put api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", api_user), params: params } + subject do + put api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", api_user, admin_mode: api_user.admin?), + params: params + end context 'when user is the admin' do let(:api_user) { admin } @@ -397,7 +425,9 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do let!(:freeze_period) { create :ci_freeze_period, project: project } let(:freeze_period_id) { freeze_period.id } - subject { delete api("/projects/#{project.id}/freeze_periods/#{freeze_period_id}", api_user) } + subject do + delete api("/projects/#{project.id}/freeze_periods/#{freeze_period_id}", api_user, admin_mode: api_user.admin?) + end context 'when user is the admin' do let(:api_user) { admin } diff --git a/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb b/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb new file mode 100644 index 00000000000..080f375245d --- /dev/null +++ b/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'UserAchievements', feature_category: :user_profile do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group, :public) } + let_it_be(:achievement) { create(:achievement, namespace: group) } + let_it_be(:non_revoked_achievement1) { create(:user_achievement, achievement: achievement, user: user) } + let_it_be(:non_revoked_achievement2) { create(:user_achievement, :revoked, achievement: achievement, user: user) } + let_it_be(:fields) do + <<~HEREDOC + id + achievements { + nodes { + userAchievements { + nodes { + id + achievement { + id + } + user { + id + } + awardedByUser { + id + } + revokedByUser { + id + } + } + } + } + } + HEREDOC + end + + let_it_be(:query) do + graphql_query_for('namespace', { full_path: group.full_path }, fields) + end + + before_all do + group.add_guest(user) + end + + before do + post_graphql(query, current_user: user) + end + + it_behaves_like 'a working graphql query' + + it 'returns all non_revoked user_achievements' do + expect(graphql_data_at(:namespace, :achievements, :nodes, :userAchievements, :nodes)) + .to contain_exactly( + a_graphql_entity_for(non_revoked_achievement1) + ) + end + + it 'can lookahead to eliminate N+1 queries', :use_clean_rails_memory_store_caching do + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: user) + end.count + + user2 = create(:user) + create(:user_achievement, achievement: achievement, user: user2) + + expect { post_graphql(query, current_user: user) }.not_to exceed_all_query_limit(control_count) + end + + context 'when the achievements feature flag is disabled' do + before do + stub_feature_flags(achievements: false) + post_graphql(query, current_user: user) + end + + specify { expect(graphql_data_at(:namespace, :achievements, :nodes, :userAchievements, :nodes)).to be_empty } + end +end diff --git a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb index 95cabfea2fc..0437a30eccd 100644 --- a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb +++ b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb @@ -50,7 +50,6 @@ RSpec.describe 'Getting Ci Cd Setting', feature_category: :continuous_integratio expect(settings_data['jobTokenScopeEnabled']).to eql project.ci_cd_settings.job_token_scope_enabled? expect(settings_data['inboundJobTokenScopeEnabled']).to eql( project.ci_cd_settings.inbound_job_token_scope_enabled?) - expect(settings_data['optInJwt']).to eql project.ci_cd_settings.opt_in_jwt? end end end diff --git a/spec/requests/api/graphql/ci/config_variables_spec.rb b/spec/requests/api/graphql/ci/config_variables_spec.rb index f76bb8ff837..4bad5dec684 100644 --- a/spec/requests/api/graphql/ci/config_variables_spec.rb +++ b/spec/requests/api/graphql/ci/config_variables_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Query.project(fullPath).ciConfigVariables(sha)', feature_category: :pipeline_authoring do +RSpec.describe 'Query.project(fullPath).ciConfigVariables(ref)', feature_category: :secrets_management do include GraphqlHelpers include ReactiveCachingHelpers @@ -20,7 +20,7 @@ RSpec.describe 'Query.project(fullPath).ciConfigVariables(sha)', feature_categor %( query { project(fullPath: "#{project.full_path}") { - ciConfigVariables(sha: "#{ref}") { + ciConfigVariables(ref: "#{ref}") { key value valueOptions diff --git a/spec/requests/api/graphql/ci/group_variables_spec.rb b/spec/requests/api/graphql/ci/group_variables_spec.rb index d78b30787c9..3b8eeefb707 100644 --- a/spec/requests/api/graphql/ci/group_variables_spec.rb +++ b/spec/requests/api/graphql/ci/group_variables_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Query.group(fullPath).ciVariables', feature_category: :pipeline_authoring do +RSpec.describe 'Query.group(fullPath).ciVariables', feature_category: :secrets_management do include GraphqlHelpers let_it_be(:group) { create(:group) } diff --git a/spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb b/spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb new file mode 100644 index 00000000000..3b4014c178c --- /dev/null +++ b/spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.project(fullPath).inheritedCiVariables', feature_category: :secrets_management do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:project) { create(:project, group: subgroup) } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + inheritedCiVariables { + nodes { + id + key + environmentScope + groupName + groupCiCdSettingsPath + masked + protected + raw + variableType + } + } + } + } + ) + end + + def create_variables + create(:ci_group_variable, group: group) + create(:ci_group_variable, group: subgroup) + end + + context 'when user is not a project maintainer' do + before do + project.add_developer(user) + end + + it 'returns nothing' do + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'inheritedCiVariables')).to be_nil + end + end + + context 'when user is a project maintainer' do + before do + project.add_maintainer(user) + end + + it "returns the project's CI variables inherited from its parent group and ancestors" do + group_var = create(:ci_group_variable, group: group, key: 'GROUP_VAR_A', + environment_scope: 'production', masked: false, protected: true, raw: true) + + subgroup_var = create(:ci_group_variable, group: subgroup, key: 'SUBGROUP_VAR_B', + masked: true, protected: false, raw: false, variable_type: 'file') + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'inheritedCiVariables', 'nodes')).to eq([ + { + 'id' => group_var.to_global_id.to_s, + 'key' => 'GROUP_VAR_A', + 'environmentScope' => 'production', + 'groupName' => group.name, + 'groupCiCdSettingsPath' => group_var.group_ci_cd_settings_path, + 'masked' => false, + 'protected' => true, + 'raw' => true, + 'variableType' => 'ENV_VAR' + }, + { + 'id' => subgroup_var.to_global_id.to_s, + 'key' => 'SUBGROUP_VAR_B', + 'environmentScope' => '*', + 'groupName' => subgroup.name, + 'groupCiCdSettingsPath' => subgroup_var.group_ci_cd_settings_path, + 'masked' => true, + 'protected' => false, + 'raw' => false, + 'variableType' => 'FILE' + } + ]) + end + + it 'avoids N+1 database queries' do + create_variables + + baseline = ActiveRecord::QueryRecorder.new do + run_with_clean_state(query, context: { current_user: user }) + end + + create_variables + + multi = ActiveRecord::QueryRecorder.new do + run_with_clean_state(query, context: { current_user: user }) + end + + expect(multi).not_to exceed_query_limit(baseline) + end + end +end diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb index 5b65ae88426..a612b4c91b6 100644 --- a/spec/requests/api/graphql/ci/instance_variables_spec.rb +++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Query.ciVariables', feature_category: :pipeline_authoring do +RSpec.describe 'Query.ciVariables', feature_category: :secrets_management do include GraphqlHelpers let(:query) do diff --git a/spec/requests/api/graphql/ci/job_spec.rb b/spec/requests/api/graphql/ci/job_spec.rb index 8121c5e5c85..960697db239 100644 --- a/spec/requests/api/graphql/ci/job_spec.rb +++ b/spec/requests/api/graphql/ci/job_spec.rb @@ -52,7 +52,8 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)', feature_category: :c 'duration' => 25, 'kind' => 'BUILD', 'queuedDuration' => 2.0, - 'status' => job_2.status.upcase + 'status' => job_2.status.upcase, + 'failureMessage' => job_2.present.failure_message ) end diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb index 674407c0a0e..0d5ac725edd 100644 --- a/spec/requests/api/graphql/ci/jobs_spec.rb +++ b/spec/requests/api/graphql/ci/jobs_spec.rb @@ -1,6 +1,130 @@ # frozen_string_literal: true require 'spec_helper' +RSpec.describe 'Query.jobs', feature_category: :continuous_integration do + include GraphqlHelpers + + let_it_be(:admin) { create(:admin) } + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:runner) { create(:ci_runner) } + let_it_be(:build) do + create(:ci_build, pipeline: pipeline, name: 'my test job', ref: 'HEAD', tag_list: %w[tag1 tag2], runner: runner) + end + + let(:query) do + %( + query { + jobs { + nodes { + id + #{fields.join(' ')} + } + } + } + ) + end + + let(:jobs_graphql_data) { graphql_data_at(:jobs, :nodes) } + + let(:fields) do + %w[commitPath refPath webPath browseArtifactsPath playPath tags runner{id}] + end + + it 'returns the paths in each job of a pipeline' do + post_graphql(query, current_user: admin) + + expect(jobs_graphql_data).to contain_exactly( + a_graphql_entity_for( + build, + commit_path: "/#{project.full_path}/-/commit/#{build.sha}", + ref_path: "/#{project.full_path}/-/commits/HEAD", + web_path: "/#{project.full_path}/-/jobs/#{build.id}", + browse_artifacts_path: "/#{project.full_path}/-/jobs/#{build.id}/artifacts/browse", + play_path: "/#{project.full_path}/-/jobs/#{build.id}/play", + tags: build.tag_list, + runner: a_graphql_entity_for(runner) + ) + ) + end + + context 'when requesting individual fields' do + using RSpec::Parameterized::TableSyntax + + let_it_be(:admin2) { create(:admin) } + let_it_be(:project2) { create(:project) } + let_it_be(:pipeline2) { create(:ci_pipeline, project: project2) } + + where(:field) { fields } + + with_them do + let(:fields) do + [field] + end + + it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do + # warm-up cache and so on: + args = { current_user: admin } + args2 = { current_user: admin2 } + post_graphql(query, **args2) + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, **args) + end + + create(:ci_build, pipeline: pipeline2, name: 'my test job2', ref: 'HEAD', tag_list: %w[tag3]) + post_graphql(query, **args) + + expect { post_graphql(query, **args) }.not_to exceed_all_query_limit(control) + end + end + end +end + +RSpec.describe 'Query.jobs.runner', feature_category: :continuous_integration do + include GraphqlHelpers + + let_it_be(:admin) { create(:admin) } + + let(:jobs_runner_graphql_data) { graphql_data_at(:jobs, :nodes, :runner) } + let(:query) do + %( + query { + jobs { + nodes { + runner{ + id + adminUrl + description + } + } + } + } + ) + end + + context 'when job has no runner' do + let_it_be(:build) { create(:ci_build) } + + it 'returns nil' do + post_graphql(query, current_user: admin) + + expect(jobs_runner_graphql_data).to eq([nil]) + end + end + + context 'when job has runner' do + let_it_be(:runner) { create(:ci_runner) } + let_it_be(:build_with_runner) { create(:ci_build, runner: runner) } + + it 'returns runner attributes' do + post_graphql(query, current_user: admin) + + expect(jobs_runner_graphql_data).to contain_exactly(a_graphql_entity_for(runner, :description, 'adminUrl' => "http://localhost/admin/runners/#{runner.id}")) + end + end +end + RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integration do include GraphqlHelpers @@ -260,6 +384,68 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati end end + describe '.jobs.runnerManager' do + let_it_be(:admin) { create(:admin) } + let_it_be(:runner_manager) { create(:ci_runner_machine, created_at: Time.current, contacted_at: Time.current) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:build) do + create(:ci_build, pipeline: pipeline, name: 'my test job', runner_manager: runner_manager) + end + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + pipeline(iid: "#{pipeline.iid}") { + jobs { + nodes { + id + name + runnerManager { + #{all_graphql_fields_for('CiRunnerManager', excluded: [:runner], max_depth: 1)} + } + } + } + } + } + } + ) + end + + let(:jobs_graphql_data) { graphql_data_at(:project, :pipeline, :jobs, :nodes) } + + it 'returns the runner manager in each job of a pipeline' do + post_graphql(query, current_user: admin) + + expect(jobs_graphql_data).to contain_exactly( + a_graphql_entity_for( + build, + name: build.name, + runner_manager: a_graphql_entity_for( + runner_manager, + system_id: runner_manager.system_xid, + created_at: runner_manager.created_at.iso8601, + contacted_at: runner_manager.contacted_at.iso8601, + status: runner_manager.status.to_s.upcase + ) + ) + ) + end + + it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do + admin2 = create(:admin) + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: admin) + end + + runner_manager2 = create(:ci_runner_machine) + create(:ci_build, pipeline: pipeline, name: 'my test job2', runner_manager: runner_manager2) + + expect { post_graphql(query, current_user: admin2) }.not_to exceed_all_query_limit(control) + end + end + describe '.jobs.count' do let_it_be(:pipeline) { create(:ci_pipeline, project: project) } let_it_be(:successful_job) { create(:ci_build, :success, pipeline: pipeline) } diff --git a/spec/requests/api/graphql/ci/manual_variables_spec.rb b/spec/requests/api/graphql/ci/manual_variables_spec.rb index 921c69e535d..47dccc0deb6 100644 --- a/spec/requests/api/graphql/ci/manual_variables_spec.rb +++ b/spec/requests/api/graphql/ci/manual_variables_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables', feature_category: :pipeline_authoring do +RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables', feature_category: :secrets_management do include GraphqlHelpers let_it_be(:project) { create(:project) } diff --git a/spec/requests/api/graphql/ci/project_variables_spec.rb b/spec/requests/api/graphql/ci/project_variables_spec.rb index 0ddcac89b34..62fc2623a0f 100644 --- a/spec/requests/api/graphql/ci/project_variables_spec.rb +++ b/spec/requests/api/graphql/ci/project_variables_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Query.project(fullPath).ciVariables', feature_category: :pipeline_authoring do +RSpec.describe 'Query.project(fullPath).ciVariables', feature_category: :secrets_management do include GraphqlHelpers let_it_be(:project) { create(:project) } diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index 986e3ce9e52..52b548ce8b9 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -6,11 +6,13 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do include GraphqlHelpers let_it_be(:user) { create(:user, :admin) } - let_it_be(:group) { create(:group) } + let_it_be(:another_admin) { create(:user, :admin) } + let_it_be_with_reload(:group) { create(:group) } let_it_be(:active_instance_runner) do - create(:ci_runner, :instance, + create(:ci_runner, :instance, :with_runner_manager, description: 'Runner 1', + creator: user, contacted_at: 2.hours.ago, active: true, version: 'adfe156', @@ -28,6 +30,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do let_it_be(:inactive_instance_runner) do create(:ci_runner, :instance, description: 'Runner 2', + creator: another_admin, contacted_at: 1.day.ago, active: false, version: 'adfe157', @@ -55,7 +58,9 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do end let_it_be(:project1) { create(:project) } - let_it_be(:active_project_runner) { create(:ci_runner, :project, projects: [project1]) } + let_it_be(:active_project_runner) do + create(:ci_runner, :project, :with_runner_manager, projects: [project1]) + end shared_examples 'runner details fetch' do let(:query) do @@ -77,6 +82,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do expect(runner_data).to match a_graphql_entity_for( runner, description: runner.description, + created_by: runner.creator ? a_graphql_entity_for(runner.creator) : nil, created_at: runner.created_at&.iso8601, contacted_at: runner.contacted_at&.iso8601, version: runner.version, @@ -85,7 +91,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do locked: false, active: runner.active, paused: !runner.active, - status: runner.status('14.5').to_s.upcase, + status: runner.status.to_s.upcase, job_execution_status: runner.builds.running.any? ? 'RUNNING' : 'IDLE', maximum_timeout: runner.maximum_timeout, access_level: runner.access_level.to_s.upcase, @@ -107,15 +113,39 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do ), project_count: nil, admin_url: "http://localhost/admin/runners/#{runner.id}", + edit_admin_url: "http://localhost/admin/runners/#{runner.id}/edit", + register_admin_url: runner.registration_available? ? "http://localhost/admin/runners/#{runner.id}/register" : nil, user_permissions: { 'readRunner' => true, 'updateRunner' => true, 'deleteRunner' => true, 'assignRunner' => true - } + }, + managers: a_hash_including( + "count" => runner.runner_managers.count, + "nodes" => an_instance_of(Array), + "pageInfo" => anything + ) ) expect(runner_data['tagList']).to match_array runner.tag_list end + + it 'does not execute more queries per runner', :use_sql_query_cache, :aggregate_failures do + # warm-up license cache and so on: + personal_access_token = create(:personal_access_token, user: user) + args = { current_user: user, token: { personal_access_token: personal_access_token } } + post_graphql(query, **args) + expect(graphql_data_at(:runner)).not_to be_nil + + personal_access_token = create(:personal_access_token, user: another_admin) + args = { current_user: another_admin, token: { personal_access_token: personal_access_token } } + control = ActiveRecord::QueryRecorder.new(skip_cached: false) { post_graphql(query, **args) } + + create(:ci_runner, :instance, version: '14.0.0', tag_list: %w[tag5 tag6], creator: another_admin) + create(:ci_runner, :project, version: '14.0.1', projects: [project1], tag_list: %w[tag3 tag8], creator: another_admin) + + expect { post_graphql(query, **args) }.not_to exceed_all_query_limit(control) + end end shared_examples 'retrieval with no admin url' do @@ -135,7 +165,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do runner_data = graphql_data_at(:runner) expect(runner_data).not_to be_nil - expect(runner_data).to match a_graphql_entity_for(runner, admin_url: nil) + expect(runner_data).to match a_graphql_entity_for(runner, admin_url: nil, edit_admin_url: nil) expect(runner_data['tagList']).to match_array runner.tag_list end end @@ -307,6 +337,24 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do it_behaves_like 'runner details fetch' end + describe 'for registration type' do + context 'when registered with registration token' do + let(:runner) do + create(:ci_runner, registration_type: :registration_token) + end + + it_behaves_like 'runner details fetch' + end + + context 'when registered with authenticated user' do + let(:runner) do + create(:ci_runner, registration_type: :authenticated_user) + end + + it_behaves_like 'runner details fetch' + end + end + describe 'for group runner request' do let(:query) do %( @@ -330,24 +378,110 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do end end - describe 'for runner with status' do - let_it_be(:stale_runner) { create(:ci_runner, description: 'Stale runner 1', created_at: 3.months.ago) } - let_it_be(:never_contacted_instance_runner) { create(:ci_runner, description: 'Missing runner 1', created_at: 1.month.ago, contacted_at: nil) } - - let(:status_fragment) do + describe 'ephemeralRegisterUrl' do + let(:runner_args) { { registration_type: :authenticated_user, creator: creator } } + let(:query) do %( - status - legacyStatusWithExplicitVersion: status(legacyMode: "14.5") - newStatus: status(legacyMode: null) + query { + runner(id: "#{runner.to_global_id}") { + ephemeralRegisterUrl + } + } ) end + shared_examples 'has register url' do + it 'retrieves register url' do + post_graphql(query, current_user: user) + expect(graphql_data_at(:runner, :ephemeral_register_url)).to eq(expected_url) + end + end + + shared_examples 'has no register url' do + it 'retrieves no register url' do + post_graphql(query, current_user: user) + expect(graphql_data_at(:runner, :ephemeral_register_url)).to eq(nil) + end + end + + context 'with an instance runner', :freeze_time do + let(:creator) { user } + let(:runner) { create(:ci_runner, **runner_args) } + + context 'with valid ephemeral registration' do + it_behaves_like 'has register url' do + let(:expected_url) { "http://localhost/admin/runners/#{runner.id}/register" } + end + end + + context 'when runner ephemeral registration has expired' do + let(:runner) do + create(:ci_runner, created_at: (Ci::Runner::REGISTRATION_AVAILABILITY_TIME + 1.second).ago, **runner_args) + end + + it_behaves_like 'has no register url' + end + + context 'when runner has already been registered' do + let(:runner) { create(:ci_runner, :with_runner_manager, **runner_args) } + + it_behaves_like 'has no register url' + end + end + + context 'with a group runner' do + let(:creator) { user } + let(:runner) { create(:ci_runner, :group, groups: [group], **runner_args) } + + context 'with valid ephemeral registration' do + it_behaves_like 'has register url' do + let(:expected_url) { "http://localhost/groups/#{group.path}/-/runners/#{runner.id}/register" } + end + end + + context 'when request not from creator' do + let(:creator) { another_admin } + + before do + group.add_owner(another_admin) + end + + it_behaves_like 'has no register url' + end + end + + context 'with a project runner' do + let(:creator) { user } + let(:runner) { create(:ci_runner, :project, projects: [project1], **runner_args) } + + context 'with valid ephemeral registration' do + it_behaves_like 'has register url' do + let(:expected_url) { "http://localhost/#{project1.full_path}/-/runners/#{runner.id}/register" } + end + end + + context 'when request not from creator' do + let(:creator) { another_admin } + + before do + project1.add_owner(another_admin) + end + + it_behaves_like 'has no register url' + end + end + end + + describe 'for runner with status' do + let_it_be(:stale_runner) { create(:ci_runner, description: 'Stale runner 1', created_at: 3.months.ago) } + let_it_be(:never_contacted_instance_runner) { create(:ci_runner, description: 'Missing runner 1', created_at: 1.month.ago, contacted_at: nil) } + let(:query) do %( query { - staleRunner: runner(id: "#{stale_runner.to_global_id}") { #{status_fragment} } - pausedRunner: runner(id: "#{inactive_instance_runner.to_global_id}") { #{status_fragment} } - neverContactedInstanceRunner: runner(id: "#{never_contacted_instance_runner.to_global_id}") { #{status_fragment} } + staleRunner: runner(id: "#{stale_runner.to_global_id}") { status } + pausedRunner: runner(id: "#{inactive_instance_runner.to_global_id}") { status } + neverContactedInstanceRunner: runner(id: "#{never_contacted_instance_runner.to_global_id}") { status } } ) end @@ -357,23 +491,17 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do stale_runner_data = graphql_data_at(:stale_runner) expect(stale_runner_data).to match a_hash_including( - 'status' => 'STALE', - 'legacyStatusWithExplicitVersion' => 'STALE', - 'newStatus' => 'STALE' + 'status' => 'STALE' ) paused_runner_data = graphql_data_at(:paused_runner) expect(paused_runner_data).to match a_hash_including( - 'status' => 'PAUSED', - 'legacyStatusWithExplicitVersion' => 'PAUSED', - 'newStatus' => 'OFFLINE' + 'status' => 'OFFLINE' ) never_contacted_instance_runner_data = graphql_data_at(:never_contacted_instance_runner) expect(never_contacted_instance_runner_data).to match a_hash_including( - 'status' => 'NEVER_CONTACTED', - 'legacyStatusWithExplicitVersion' => 'NEVER_CONTACTED', - 'newStatus' => 'NEVER_CONTACTED' + 'status' => 'NEVER_CONTACTED' ) end end @@ -568,34 +696,34 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do end end - context 'with request made by creator' do + context 'with request made by creator', :frozen_time do let(:user) { creator } context 'with runner created in UI' do let(:registration_type) { :authenticated_user } - context 'with runner created in last 3 hours' do - let(:created_at) { (3.hours - 1.second).ago } + context 'with runner created in last hour' do + let(:created_at) { (Ci::Runner::REGISTRATION_AVAILABILITY_TIME - 1.second).ago } - context 'with no runner machine registed yet' do + context 'with no runner manager registered yet' do it_behaves_like 'an ephemeral_authentication_token' end - context 'with first runner machine already registed' do - let!(:runner_machine) { create(:ci_runner_machine, runner: runner) } + context 'with first runner manager already registered' do + let!(:runner_manager) { create(:ci_runner_machine, runner: runner) } it_behaves_like 'a protected ephemeral_authentication_token' end end context 'with runner created almost too long ago' do - let(:created_at) { (3.hours - 1.second).ago } + let(:created_at) { (Ci::Runner::REGISTRATION_AVAILABILITY_TIME - 1.second).ago } it_behaves_like 'an ephemeral_authentication_token' end context 'with runner created too long ago' do - let(:created_at) { 3.hours.ago } + let(:created_at) { Ci::Runner::REGISTRATION_AVAILABILITY_TIME.ago } it_behaves_like 'a protected ephemeral_authentication_token' end @@ -604,8 +732,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do context 'with runner registered from command line' do let(:registration_type) { :registration_token } - context 'with runner created in last 3 hours' do - let(:created_at) { (3.hours - 1.second).ago } + context 'with runner created in last 1 hour' do + let(:created_at) { (Ci::Runner::REGISTRATION_AVAILABILITY_TIME - 1.second).ago } it_behaves_like 'a protected ephemeral_authentication_token' end @@ -628,6 +756,12 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do <<~SINGLE runner(id: "#{runner.to_global_id}") { #{all_graphql_fields_for('CiRunner', excluded: excluded_fields)} + createdBy { + id + username + webPath + webUrl + } groups { nodes { id @@ -658,7 +792,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do let(:active_group_runner2) { create(:ci_runner, :group) } # Exclude fields that are already hardcoded above - let(:excluded_fields) { %w[jobs groups projects ownerProject] } + let(:excluded_fields) { %w[createdBy jobs groups projects ownerProject] } let(:single_query) do <<~QUERY @@ -691,6 +825,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, **args) } + personal_access_token = create(:personal_access_token, user: another_admin) + args = { current_user: another_admin, token: { personal_access_token: personal_access_token } } expect { post_graphql(double_query, **args) }.not_to exceed_query_limit(control) expect(graphql_data.count).to eq 6 @@ -721,20 +857,20 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do end describe 'Query limits with jobs' do - let!(:group1) { create(:group) } - let!(:group2) { create(:group) } - let!(:project1) { create(:project, :repository, group: group1) } - let!(:project2) { create(:project, :repository, group: group1) } - let!(:project3) { create(:project, :repository, group: group2) } + let_it_be(:group1) { create(:group) } + let_it_be(:group2) { create(:group) } + let_it_be(:project1) { create(:project, :repository, group: group1) } + let_it_be(:project2) { create(:project, :repository, group: group1) } + let_it_be(:project3) { create(:project, :repository, group: group2) } - let!(:merge_request1) { create(:merge_request, source_project: project1) } - let!(:merge_request2) { create(:merge_request, source_project: project3) } + let_it_be(:merge_request1) { create(:merge_request, source_project: project1) } + let_it_be(:merge_request2) { create(:merge_request, source_project: project3) } let(:project_runner2) { create(:ci_runner, :project, projects: [project1, project2]) } let!(:build1) { create(:ci_build, :success, name: 'Build One', runner: project_runner2, pipeline: pipeline1) } - let!(:pipeline1) do + let_it_be(:pipeline1) do create(:ci_pipeline, project: project1, source: :merge_request_event, merge_request: merge_request1, ref: 'main', - target_sha: 'xxx') + target_sha: 'xxx') end let(:query) do @@ -745,24 +881,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do jobs { nodes { id - detailedStatus { - id - detailsPath - group - icon - text - } - project { - id - name - webUrl - } - shortSha - commitPath - finishedAt - duration - queuedDuration - tags + #{field} } } } @@ -770,42 +889,69 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do QUERY end - it 'does not execute more queries per job', :aggregate_failures do - # warm-up license cache and so on: - personal_access_token = create(:personal_access_token, user: user) - args = { current_user: user, token: { personal_access_token: personal_access_token } } - post_graphql(query, **args) - - control = ActiveRecord::QueryRecorder.new(query_recorder_debug: true) { post_graphql(query, **args) } - - # Add a new build to project_runner2 - project_runner2.runner_projects << build(:ci_runner_project, runner: project_runner2, project: project3) - pipeline2 = create(:ci_pipeline, project: project3, source: :merge_request_event, merge_request: merge_request2, - ref: 'main', target_sha: 'xxx') - build2 = create(:ci_build, :success, name: 'Build Two', runner: project_runner2, pipeline: pipeline2) + context 'when requesting individual fields' do + using RSpec::Parameterized::TableSyntax - args[:current_user] = create(:user, :admin) # do not reuse same user - expect { post_graphql(query, **args) }.not_to exceed_all_query_limit(control) + where(:field) do + [ + 'detailedStatus { id detailsPath group icon text }', + 'project { id name webUrl }' + ] + %w[ + shortSha + browseArtifactsPath + commitPath + playPath + refPath + webPath + finishedAt + duration + queuedDuration + tags + ] + end - expect(graphql_data.count).to eq 1 - expect(graphql_data).to match( - a_hash_including( - 'runner' => a_graphql_entity_for( - project_runner2, - jobs: { 'nodes' => containing_exactly(a_graphql_entity_for(build1), a_graphql_entity_for(build2)) } - ) - )) + with_them do + it 'does not execute more queries per job', :use_sql_query_cache, :aggregate_failures do + admin2 = create(:user, :admin) # do not reuse same user + + # warm-up license cache and so on: + personal_access_token = create(:personal_access_token, user: user) + personal_access_token2 = create(:personal_access_token, user: admin2) + args = { current_user: user, token: { personal_access_token: personal_access_token } } + args2 = { current_user: admin2, token: { personal_access_token: personal_access_token2 } } + post_graphql(query, **args2) + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) { post_graphql(query, **args) } + + # Add a new build to project_runner2 + project_runner2.runner_projects << build(:ci_runner_project, runner: project_runner2, project: project3) + pipeline2 = create(:ci_pipeline, project: project3, source: :merge_request_event, merge_request: merge_request2, + ref: 'main', target_sha: 'xxx') + build2 = create(:ci_build, :success, name: 'Build Two', runner: project_runner2, pipeline: pipeline2) + + expect { post_graphql(query, **args2) }.not_to exceed_all_query_limit(control) + + expect(graphql_data.count).to eq 1 + expect(graphql_data).to match( + a_hash_including( + 'runner' => a_graphql_entity_for( + project_runner2, + jobs: { 'nodes' => containing_exactly(a_graphql_entity_for(build1), a_graphql_entity_for(build2)) } + ) + )) + end + end end end describe 'sorting and pagination' do let(:query) do <<~GQL - query($id: CiRunnerID!, $projectSearchTerm: String, $n: Int, $cursor: String) { - runner(id: $id) { - #{fields} + query($id: CiRunnerID!, $projectSearchTerm: String, $n: Int, $cursor: String) { + runner(id: $id) { + #{fields} + } } - } GQL end @@ -824,18 +970,18 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do let(:fields) do <<~QUERY - projects(search: $projectSearchTerm, first: $n, after: $cursor) { - count - nodes { - id - } - pageInfo { - hasPreviousPage - startCursor - endCursor - hasNextPage + projects(search: $projectSearchTerm, first: $n, after: $cursor) { + count + nodes { + id + } + pageInfo { + hasPreviousPage + startCursor + endCursor + hasNextPage + } } - } QUERY end diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb index 75d8609dc38..c8706ae9698 100644 --- a/spec/requests/api/graphql/ci/runners_spec.rb +++ b/spec/requests/api/graphql/ci/runners_spec.rb @@ -11,16 +11,24 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do let_it_be(:instance_runner) { create(:ci_runner, :instance, version: 'abc', revision: '123', description: 'Instance runner', ip_address: '127.0.0.1') } let_it_be(:project_runner) { create(:ci_runner, :project, active: false, version: 'def', revision: '456', description: 'Project runner', projects: [project], ip_address: '127.0.0.1') } - let(:runners_graphql_data) { graphql_data['runners'] } + let(:runners_graphql_data) { graphql_data_at(:runners) } let(:params) { {} } let(:fields) do <<~QUERY nodes { - #{all_graphql_fields_for('CiRunner', excluded: %w[ownerProject])} + #{all_graphql_fields_for('CiRunner', excluded: %w[createdBy ownerProject])} + createdBy { + username + webPath + webUrl + } ownerProject { id + path + fullPath + webUrl } } QUERY @@ -50,6 +58,25 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do it 'returns expected runner' do expect(runners_graphql_data['nodes']).to contain_exactly(a_graphql_entity_for(expected_runner)) end + + it 'does not execute more queries per runner', :aggregate_failures do + # warm-up license cache and so on: + personal_access_token = create(:personal_access_token, user: current_user) + args = { current_user: current_user, token: { personal_access_token: personal_access_token } } + post_graphql(query, **args) + expect(graphql_data_at(:runners, :nodes)).not_to be_empty + + admin2 = create(:admin) + personal_access_token = create(:personal_access_token, user: admin2) + args = { current_user: admin2, token: { personal_access_token: personal_access_token } } + control = ActiveRecord::QueryRecorder.new { post_graphql(query, **args) } + + create(:ci_runner, :instance, version: '14.0.0', tag_list: %w[tag5 tag6], creator: admin2) + create(:ci_runner, :project, version: '14.0.1', projects: [project], tag_list: %w[tag3 tag8], + creator: current_user) + + expect { post_graphql(query, **args) }.not_to exceed_query_limit(control) + end end context 'runner_type is INSTANCE_TYPE and status is ACTIVE' do diff --git a/spec/requests/api/graphql/current_user/todos_query_spec.rb b/spec/requests/api/graphql/current_user/todos_query_spec.rb index f7e23aeb241..ee019a99f8d 100644 --- a/spec/requests/api/graphql/current_user/todos_query_spec.rb +++ b/spec/requests/api/graphql/current_user/todos_query_spec.rb @@ -19,7 +19,7 @@ RSpec.describe 'Query current user todos', feature_category: :source_code_manage let(:fields) do <<~QUERY nodes { - #{all_graphql_fields_for('todos'.classify, max_depth: 2)} + #{all_graphql_fields_for('todos'.classify, max_depth: 2, excluded: ['productAnalyticsState'])} } QUERY end diff --git a/spec/requests/api/graphql/current_user_query_spec.rb b/spec/requests/api/graphql/current_user_query_spec.rb index 53d2580caee..aceef77920d 100644 --- a/spec/requests/api/graphql/current_user_query_spec.rb +++ b/spec/requests/api/graphql/current_user_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'getting project information', feature_category: :authentication_and_authorization do +RSpec.describe 'getting project information', feature_category: :system_access do include GraphqlHelpers let(:fields) do diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb index 7b804623e01..1858ea831dd 100644 --- a/spec/requests/api/graphql/custom_emoji_query_spec.rb +++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'getting custom emoji within namespace', feature_category: :not_owned do +RSpec.describe 'getting custom emoji within namespace', feature_category: :shared do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/group/data_transfer_spec.rb b/spec/requests/api/graphql/group/data_transfer_spec.rb new file mode 100644 index 00000000000..b7c038afa54 --- /dev/null +++ b/spec/requests/api/graphql/group/data_transfer_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'group data transfers', feature_category: :source_code_management do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project_1) { create(:project, group: group) } + let_it_be(:project_2) { create(:project, group: group) } + + let(:fields) do + <<~QUERY + #{all_graphql_fields_for('GroupDataTransfer'.classify)} + QUERY + end + + let(:query) do + graphql_query_for( + 'group', + { fullPath: group.full_path }, + query_graphql_field('DataTransfer', params, fields) + ) + end + + let(:from) { Date.new(2022, 1, 1) } + let(:to) { Date.new(2023, 1, 1) } + let(:params) { { from: from, to: to } } + let(:egress_data) do + graphql_data.dig('group', 'dataTransfer', 'egressNodes', 'nodes') + end + + before do + create(:project_data_transfer, project: project_1, date: '2022-01-01', repository_egress: 1) + create(:project_data_transfer, project: project_1, date: '2022-02-01', repository_egress: 2) + create(:project_data_transfer, project: project_2, date: '2022-02-01', repository_egress: 4) + end + + subject { post_graphql(query, current_user: current_user) } + + context 'with anonymous access' do + let_it_be(:current_user) { nil } + + before do + subject + end + + it_behaves_like 'a working graphql query' + + it 'returns no data' do + expect(graphql_data_at(:group, :data_transfer)).to be_nil + expect(graphql_errors).to be_nil + end + end + + context 'with authorized user but without enough permissions' do + before do + group.add_developer(current_user) + subject + end + + it_behaves_like 'a working graphql query' + + it 'returns empty results' do + expect(graphql_data_at(:group, :data_transfer)).to be_nil + expect(graphql_errors).to be_nil + end + end + + context 'when user has enough permissions' do + before do + group.add_owner(current_user) + end + + context 'when data_transfer_monitoring_mock_data is NOT enabled' do + before do + stub_feature_flags(data_transfer_monitoring_mock_data: false) + subject + end + + it 'returns real results' do + expect(response).to have_gitlab_http_status(:ok) + + expect(egress_data.count).to eq(2) + + expect(egress_data.first.keys).to match_array( + %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress] + ) + + expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 6]) + end + + it_behaves_like 'a working graphql query' + end + + context 'when data_transfer_monitoring_mock_data is enabled' do + before do + stub_feature_flags(data_transfer_monitoring_mock_data: true) + subject + end + + it 'returns mock results' do + expect(response).to have_gitlab_http_status(:ok) + + expect(egress_data.count).to eq(12) + expect(egress_data.first.keys).to match_array( + %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress] + ) + end + + it_behaves_like 'a working graphql query' + end + end +end diff --git a/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb index 2c4770a31a7..a6eb114a279 100644 --- a/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb +++ b/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb @@ -26,6 +26,7 @@ RSpec.describe 'getting dependency proxy blobs in a group', feature_category: :d #{query_graphql_field('dependency_proxy_blobs', {}, dependency_proxy_blob_fields)} dependencyProxyBlobCount dependencyProxyTotalSize + dependencyProxyTotalSizeInBytes GQL end @@ -42,6 +43,7 @@ RSpec.describe 'getting dependency proxy blobs in a group', feature_category: :d let(:dependency_proxy_blobs_response) { graphql_data.dig('group', 'dependencyProxyBlobs', 'edges') } let(:dependency_proxy_blob_count_response) { graphql_data.dig('group', 'dependencyProxyBlobCount') } let(:dependency_proxy_total_size_response) { graphql_data.dig('group', 'dependencyProxyTotalSize') } + let(:dependency_proxy_total_size_in_bytes_response) { graphql_data.dig('group', 'dependencyProxyTotalSizeInBytes') } before do stub_config(dependency_proxy: { enabled: true }) @@ -121,7 +123,13 @@ RSpec.describe 'getting dependency proxy blobs in a group', feature_category: :d it 'returns the total size' do subject + expected_size = ActiveSupport::NumberHelper.number_to_human_size(blobs.inject(0) { |sum, blob| sum + blob.size }) + expect(dependency_proxy_total_size_response).to eq(expected_size) + end + + it 'returns the total size in bytes' do + subject expected_size = blobs.inject(0) { |sum, blob| sum + blob.size } - expect(dependency_proxy_total_size_response).to eq(ActiveSupport::NumberHelper.number_to_human_size(expected_size)) + expect(dependency_proxy_total_size_in_bytes_response).to eq(expected_size) end end diff --git a/spec/requests/api/graphql/group/labels_query_spec.rb b/spec/requests/api/graphql/group/labels_query_spec.rb deleted file mode 100644 index 28886f8d80b..00000000000 --- a/spec/requests/api/graphql/group/labels_query_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'getting group label information', feature_category: :team_planning do - include GraphqlHelpers - - let_it_be(:group) { create(:group, :public) } - let_it_be(:label_factory) { :group_label } - let_it_be(:label_attrs) { { group: group } } - - it_behaves_like 'querying a GraphQL type with labels' do - let(:path_prefix) { ['group'] } - - def make_query(fields) - graphql_query_for('group', { full_path: group.full_path }, fields) - end - end -end diff --git a/spec/requests/api/graphql/group/milestones_spec.rb b/spec/requests/api/graphql/group/milestones_spec.rb index 28cd68493c0..209588835f2 100644 --- a/spec/requests/api/graphql/group/milestones_spec.rb +++ b/spec/requests/api/graphql/group/milestones_spec.rb @@ -35,12 +35,6 @@ RSpec.describe 'Milestones through GroupQuery', feature_category: :team_planning end context 'when filtering by timeframe' do - it 'fetches milestones between start_date and due_date' do - fetch_milestones(user, { start_date: now.to_s, end_date: (now + 2.days).to_s }) - - expect_array_response(milestone_2.to_global_id.to_s, milestone_3.to_global_id.to_s) - end - it 'fetches milestones between timeframe start and end arguments' do today = Date.today fetch_milestones(user, { timeframe: { start: today.to_s, end: (today + 2.days).to_s } }) diff --git a/spec/requests/api/graphql/issues_spec.rb b/spec/requests/api/graphql/issues_spec.rb index e437e1bbcb0..a12049a9b2e 100644 --- a/spec/requests/api/graphql/issues_spec.rb +++ b/spec/requests/api/graphql/issues_spec.rb @@ -15,6 +15,7 @@ RSpec.describe 'getting an issue list at root level', feature_category: :team_pl let_it_be(:project_b) { create(:project, :repository, :private, group: group1) } let_it_be(:project_c) { create(:project, :repository, :public, group: group2) } let_it_be(:project_d) { create(:project, :repository, :private, group: group2) } + let_it_be(:archived_project) { create(:project, :repository, :archived, group: group2) } let_it_be(:milestone1) { create(:milestone, project: project_c, due_date: 10.days.from_now) } let_it_be(:milestone2) { create(:milestone, project: project_d, due_date: 20.days.from_now) } let_it_be(:milestone3) { create(:milestone, project: project_d, due_date: 30.days.from_now) } @@ -83,6 +84,7 @@ RSpec.describe 'getting an issue list at root level', feature_category: :team_pl ) end + let_it_be(:archived_issue) { create(:issue, project: archived_project) } let_it_be(:issues, reload: true) { [issue_a, issue_b, issue_c, issue_d, issue_e] } # we need to always provide at least one filter to the query so it doesn't fail let_it_be(:base_params) { { iids: issues.map { |issue| issue.iid.to_s } } } @@ -109,6 +111,38 @@ RSpec.describe 'getting an issue list at root level', feature_category: :team_pl end end + describe 'includeArchived filter' do + let(:base_params) { { iids: [archived_issue.iid.to_s] } } + + it 'excludes issues from archived projects' do + post_query + + issue_ids = graphql_dig_at(graphql_data_at('issues', 'nodes'), :id) + + expect(issue_ids).not_to include(archived_issue.to_gid.to_s) + end + + context 'when includeArchived is true' do + let(:issue_filter_params) { { include_archived: true } } + + it 'includes issues from archived projects' do + post_query + + issue_ids = graphql_dig_at(graphql_data_at('issues', 'nodes'), :id) + + expect(issue_ids).to include(archived_issue.to_gid.to_s) + end + end + end + + it 'excludes issues from archived projects' do + post_query + + issue_ids = graphql_dig_at(graphql_data_at('issues', 'nodes'), :id) + + expect(issue_ids).not_to include(archived_issue.to_gid.to_s) + end + context 'when no filters are provided' do let(:all_query_params) { {} } diff --git a/spec/requests/api/graphql/jobs_query_spec.rb b/spec/requests/api/graphql/jobs_query_spec.rb index 0aea8e4c253..7607aeac6e0 100644 --- a/spec/requests/api/graphql/jobs_query_spec.rb +++ b/spec/requests/api/graphql/jobs_query_spec.rb @@ -5,17 +5,26 @@ require 'spec_helper' RSpec.describe 'getting job information', feature_category: :continuous_integration do include GraphqlHelpers - let_it_be(:job) { create(:ci_build, :success, name: 'job1') } - let(:query) do - graphql_query_for(:jobs) + graphql_query_for( + :jobs, {}, %( + count + nodes { + #{all_graphql_fields_for(::Types::Ci::JobType, max_depth: 1)} + }) + ) end + let_it_be(:runner) { create(:ci_runner) } + let_it_be(:job) { create(:ci_build, :success, name: 'job1', runner: runner) } + + subject(:request) { post_graphql(query, current_user: current_user) } + context 'when user is admin' do let_it_be(:current_user) { create(:admin) } - it 'has full access to all jobs', :aggregate_failure do - post_graphql(query, current_user: current_user) + it 'has full access to all jobs', :aggregate_failures do + request expect(graphql_data_at(:jobs, :count)).to eq(1) expect(graphql_data_at(:jobs, :nodes)).to contain_exactly(a_graphql_entity_for(job)) @@ -25,14 +34,14 @@ RSpec.describe 'getting job information', feature_category: :continuous_integrat let_it_be(:pending_job) { create(:ci_build, :pending) } let_it_be(:failed_job) { create(:ci_build, :failed) } - it 'gets pending jobs', :aggregate_failure do + it 'gets pending jobs', :aggregate_failures do post_graphql(graphql_query_for(:jobs, { statuses: :PENDING }), current_user: current_user) expect(graphql_data_at(:jobs, :count)).to eq(1) expect(graphql_data_at(:jobs, :nodes)).to contain_exactly(a_graphql_entity_for(pending_job)) end - it 'gets pending and failed jobs', :aggregate_failure do + it 'gets pending and failed jobs', :aggregate_failures do post_graphql(graphql_query_for(:jobs, { statuses: [:PENDING, :FAILED] }), current_user: current_user) expect(graphql_data_at(:jobs, :count)).to eq(2) @@ -40,13 +49,27 @@ RSpec.describe 'getting job information', feature_category: :continuous_integrat a_graphql_entity_for(failed_job)]) end end + + context 'when N+1 queries' do + it 'avoids N+1 queries successfully', :use_sql_query_cache do + post_graphql(query, current_user: current_user) # warmup + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: current_user) + end + + create(:ci_build, :success, name: 'job2', runner: create(:ci_runner)) + + expect { post_graphql(query, current_user: current_user) }.not_to exceed_all_query_limit(control) + end + end end context 'if the user is not an admin' do let_it_be(:current_user) { create(:user) } - it 'has no access to the jobs', :aggregate_failure do - post_graphql(query, current_user: current_user) + it 'has no access to the jobs', :aggregate_failures do + request expect(graphql_data_at(:jobs, :count)).to eq(0) expect(graphql_data_at(:jobs, :nodes)).to match_array([]) diff --git a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb index 4dd47142c40..143bc1672f8 100644 --- a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb +++ b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb @@ -22,6 +22,7 @@ RSpec.describe 'Getting Metrics Dashboard Annotations', feature_category: :metri create(:metrics_dashboard_annotation, environment: environment, starting_at: to.advance(minutes: 5), dashboard_path: path) end + let(:remove_monitor_metrics) { false } let(:args) { "from: \"#{from}\", to: \"#{to}\"" } let(:fields) do <<~QUERY @@ -50,6 +51,7 @@ RSpec.describe 'Getting Metrics Dashboard Annotations', feature_category: :metri end before do + stub_feature_flags(remove_monitor_metrics: remove_monitor_metrics) project.add_developer(current_user) post_graphql(query, current_user: current_user) end @@ -85,4 +87,18 @@ RSpec.describe 'Getting Metrics Dashboard Annotations', feature_category: :metri it_behaves_like 'a working graphql query' end end + + context 'when metrics dashboard feature is unavailable' do + let(:remove_monitor_metrics) { true } + + it_behaves_like 'a working graphql query' + + it 'returns nil' do + annotations = graphql_data.dig( + 'project', 'environments', 'nodes', 0, 'metricsDashboard', 'annotations' + ) + + expect(annotations).to be_nil + end + end end diff --git a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb index 8db0844c6d7..b7d9b59f5fe 100644 --- a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb +++ b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb @@ -45,7 +45,10 @@ RSpec.describe 'Getting Metrics Dashboard', feature_category: :metrics do end context 'for user with developer access' do + let(:remove_monitor_metrics) { false } + before do + stub_feature_flags(remove_monitor_metrics: remove_monitor_metrics) project.add_developer(current_user) post_graphql(query, current_user: current_user) end @@ -82,6 +85,18 @@ RSpec.describe 'Getting Metrics Dashboard', feature_category: :metrics do expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: should be an array of panel_groups objects"]) end end + + context 'metrics dashboard feature is unavailable' do + let(:remove_monitor_metrics) { true } + + it_behaves_like 'a working graphql query' + + it 'returns nil' do + dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') + + expect(dashboard).to be_nil + end + end end context 'requested dashboard can not be found' do diff --git a/spec/requests/api/graphql/multiplexed_queries_spec.rb b/spec/requests/api/graphql/multiplexed_queries_spec.rb index 4d615d3eaa4..0a5c87ebef8 100644 --- a/spec/requests/api/graphql/multiplexed_queries_spec.rb +++ b/spec/requests/api/graphql/multiplexed_queries_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe 'Multiplexed queries', feature_category: :not_owned do +RSpec.describe 'Multiplexed queries', feature_category: :shared do include GraphqlHelpers it 'returns responses for multiple queries' do diff --git a/spec/requests/api/graphql/mutations/achievements/award_spec.rb b/spec/requests/api/graphql/mutations/achievements/award_spec.rb new file mode 100644 index 00000000000..9bc0751e924 --- /dev/null +++ b/spec/requests/api/graphql/mutations/achievements/award_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Achievements::Award, feature_category: :user_profile do + include GraphqlHelpers + + let_it_be(:developer) { create(:user) } + let_it_be(:maintainer) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:achievement) { create(:achievement, namespace: group) } + let_it_be(:recipient) { create(:user) } + + let(:mutation) { graphql_mutation(:achievements_award, params) } + let(:achievement_id) { achievement&.to_global_id } + let(:recipient_id) { recipient&.to_global_id } + let(:params) do + { + achievement_id: achievement_id, + user_id: recipient_id + } + end + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + def mutation_response + graphql_mutation_response(:achievements_create) + end + + before_all do + group.add_developer(developer) + group.add_maintainer(maintainer) + end + + context 'when the user does not have permission' do + let(:current_user) { developer } + + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not create an achievement' do + expect { subject }.not_to change { Achievements::UserAchievement.count } + end + end + + context 'when the user has permission' do + let(:current_user) { maintainer } + + context 'when the params are invalid' do + let(:achievement) { nil } + + it 'returns the validation error' do + subject + + expect(graphql_errors.to_s).to include('invalid value for achievementId (Expected value to not be null)') + end + end + + context 'when the recipient_id is invalid' do + let(:recipient_id) { "gid://gitlab/User/#{non_existing_record_id}" } + + it 'returns the validation error' do + subject + + expect(graphql_data_at(:achievements_award, + :errors)).to include("Couldn't find User with 'id'=#{non_existing_record_id}") + end + end + + context 'when the achievement_id is invalid' do + let(:achievement_id) { "gid://gitlab/Achievements::Achievement/#{non_existing_record_id}" } + + it 'returns the validation error' do + subject + + expect(graphql_errors.to_s) + .to include("The resource that you are attempting to access does not exist or you don't have permission") + end + end + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(achievements: false) + end + + it 'returns the relevant error' do + subject + + expect(graphql_errors.to_s) + .to include("The resource that you are attempting to access does not exist or you don't have permission") + end + end + + it 'creates an achievement' do + expect { subject }.to change { Achievements::UserAchievement.count }.by(1) + end + + it 'returns the new achievement' do + subject + + expect(graphql_data_at(:achievements_award, :user_achievement, :achievement, :id)) + .to eq(achievement.to_global_id.to_s) + expect(graphql_data_at(:achievements_award, :user_achievement, :user, :id)) + .to eq(recipient.to_global_id.to_s) + end + end +end diff --git a/spec/requests/api/graphql/mutations/achievements/delete_spec.rb b/spec/requests/api/graphql/mutations/achievements/delete_spec.rb new file mode 100644 index 00000000000..276da4f46a8 --- /dev/null +++ b/spec/requests/api/graphql/mutations/achievements/delete_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Achievements::Delete, feature_category: :user_profile do + include GraphqlHelpers + + let_it_be(:developer) { create(:user) } + let_it_be(:maintainer) { create(:user) } + let_it_be(:group) { create(:group) } + + let!(:achievement) { create(:achievement, namespace: group) } + let(:mutation) { graphql_mutation(:achievements_delete, params) } + let(:achievement_id) { achievement&.to_global_id } + let(:params) { { achievement_id: achievement_id } } + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + def mutation_response + graphql_mutation_response(:achievements_delete) + end + + before_all do + group.add_developer(developer) + group.add_maintainer(maintainer) + end + + context 'when the user does not have permission' do + let(:current_user) { developer } + + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not revoke any achievements' do + expect { subject }.not_to change { Achievements::Achievement.count } + end + end + + context 'when the user has permission' do + let(:current_user) { maintainer } + + context 'when the params are invalid' do + let(:achievement) { nil } + + it 'returns the validation error' do + subject + + expect(graphql_errors.to_s).to include('invalid value for achievementId (Expected value to not be null)') + end + end + + context 'when the achievement_id is invalid' do + let(:achievement_id) { "gid://gitlab/Achievements::Achievement/#{non_existing_record_id}" } + + it 'returns the validation error' do + subject + + expect(graphql_errors.to_s) + .to include("The resource that you are attempting to access does not exist or you don't have permission") + end + end + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(achievements: false) + end + + it 'returns the relevant error' do + subject + + expect(graphql_errors.to_s) + .to include("The resource that you are attempting to access does not exist or you don't have permission") + end + end + + it 'deletes the achievement' do + expect { subject }.to change { Achievements::Achievement.count }.by(-1) + end + end +end diff --git a/spec/requests/api/graphql/mutations/achievements/revoke_spec.rb b/spec/requests/api/graphql/mutations/achievements/revoke_spec.rb new file mode 100644 index 00000000000..925a1bb9fcc --- /dev/null +++ b/spec/requests/api/graphql/mutations/achievements/revoke_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Achievements::Revoke, feature_category: :user_profile do + include GraphqlHelpers + + let_it_be(:developer) { create(:user) } + let_it_be(:maintainer) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:achievement) { create(:achievement, namespace: group) } + let_it_be(:user_achievement) { create(:user_achievement, achievement: achievement) } + + let(:mutation) { graphql_mutation(:achievements_revoke, params) } + let(:user_achievement_id) { user_achievement&.to_global_id } + let(:params) { { user_achievement_id: user_achievement_id } } + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + def mutation_response + graphql_mutation_response(:achievements_create) + end + + before_all do + group.add_developer(developer) + group.add_maintainer(maintainer) + end + + context 'when the user does not have permission' do + let(:current_user) { developer } + + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not revoke any achievements' do + expect { subject }.not_to change { Achievements::UserAchievement.where(revoked_by_user_id: nil).count } + end + end + + context 'when the user has permission' do + let(:current_user) { maintainer } + + context 'when the params are invalid' do + let(:user_achievement) { nil } + + it 'returns the validation error' do + subject + + expect(graphql_errors.to_s).to include('invalid value for userAchievementId (Expected value to not be null)') + end + end + + context 'when the user_achievement_id is invalid' do + let(:user_achievement_id) { "gid://gitlab/Achievements::UserAchievement/#{non_existing_record_id}" } + + it 'returns the validation error' do + subject + + expect(graphql_errors.to_s) + .to include("The resource that you are attempting to access does not exist or you don't have permission") + end + end + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(achievements: false) + end + + it 'returns the relevant error' do + subject + + expect(graphql_errors.to_s) + .to include("The resource that you are attempting to access does not exist or you don't have permission") + end + end + + it 'revokes an achievement' do + expect { subject }.to change { Achievements::UserAchievement.where(revoked_by_user_id: nil).count }.by(-1) + end + + it 'returns the revoked achievement' do + subject + + expect(graphql_data_at(:achievements_revoke, :user_achievement, :achievement, :id)) + .to eq(achievement.to_global_id.to_s) + expect(graphql_data_at(:achievements_revoke, :user_achievement, :revoked_by_user, :id)) + .to eq(current_user.to_global_id.to_s) + expect(graphql_data_at(:achievements_revoke, :user_achievement, :revoked_at)) + .not_to be_nil + end + end +end diff --git a/spec/requests/api/graphql/mutations/achievements/update_spec.rb b/spec/requests/api/graphql/mutations/achievements/update_spec.rb new file mode 100644 index 00000000000..b2bb01b564c --- /dev/null +++ b/spec/requests/api/graphql/mutations/achievements/update_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Achievements::Update, feature_category: :user_profile do + include GraphqlHelpers + include WorkhorseHelpers + + let_it_be(:developer) { create(:user) } + let_it_be(:maintainer) { create(:user) } + let_it_be(:group) { create(:group) } + + let!(:achievement) { create(:achievement, namespace: group) } + let(:mutation) { graphql_mutation(:achievements_update, params) } + let(:achievement_id) { achievement&.to_global_id } + let(:params) { { achievement_id: achievement_id, name: 'GitLab', avatar: avatar } } + let(:avatar) { nil } + + subject { post_graphql_mutation_with_uploads(mutation, current_user: current_user) } + + def mutation_response + graphql_mutation_response(:achievements_update) + end + + before_all do + group.add_developer(developer) + group.add_maintainer(maintainer) + end + + context 'when the user does not have permission' do + let(:current_user) { developer } + + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not update the achievement' do + expect { subject }.not_to change { achievement.reload.name } + end + end + + context 'when the user has permission' do + let(:current_user) { maintainer } + + context 'when the params are invalid' do + let(:achievement) { nil } + + it 'returns the validation error' do + subject + + expect(graphql_errors.to_s).to include('invalid value for achievementId (Expected value to not be null)') + end + end + + context 'when the achievement_id is invalid' do + let(:achievement_id) { "gid://gitlab/Achievements::Achievement/#{non_existing_record_id}" } + + it 'returns the validation error' do + subject + + expect(graphql_errors.to_s) + .to include("The resource that you are attempting to access does not exist or you don't have permission") + end + end + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(achievements: false) + end + + it 'returns the relevant permission error' do + subject + + expect(graphql_errors.to_s) + .to include("The resource that you are attempting to access does not exist or you don't have permission") + end + end + + context 'with a new avatar' do + let(:avatar) { fixture_file_upload("spec/fixtures/dk.png") } + + it 'updates the achievement' do + subject + + achievement.reload + + expect(achievement.name).to eq('GitLab') + expect(achievement.avatar.file).not_to be_nil + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb index 64ea6d32f5f..b3d25155a6f 100644 --- a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb +++ b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_category: :not_owned do +RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_category: :shared do include GraphqlHelpers let_it_be(:admin) { create(:admin) } diff --git a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb index fdbff0f93cd..18cc85d36e0 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Adding an AwardEmoji', feature_category: :not_owned do +RSpec.describe 'Adding an AwardEmoji', feature_category: :shared do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb index e200bfc2d18..7ec2b061a88 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Removing an AwardEmoji', feature_category: :not_owned do +RSpec.describe 'Removing an AwardEmoji', feature_category: :shared do include GraphqlHelpers let(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb index 6dba2b58357..7c6a487cdd0 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Toggling an AwardEmoji', feature_category: :not_owned do +RSpec.describe 'Toggling an AwardEmoji', feature_category: :shared do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb b/spec/requests/api/graphql/mutations/ci/job/cancel_spec.rb index 468a9e57f56..abad1ae0812 100644 --- a/spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/job/cancel_spec.rb @@ -15,12 +15,12 @@ RSpec.describe "JobCancel", feature_category: :continuous_integration do id: job.to_global_id.to_s } graphql_mutation(:job_cancel, variables, - <<-QL + <<-QL errors job { id } - QL + QL ) end diff --git a/spec/requests/api/graphql/mutations/ci/job_play_spec.rb b/spec/requests/api/graphql/mutations/ci/job/play_spec.rb index 9ba80e51dee..0c700248f85 100644 --- a/spec/requests/api/graphql/mutations/ci/job_play_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/job/play_spec.rb @@ -18,7 +18,7 @@ RSpec.describe 'JobPlay', feature_category: :continuous_integration do let(:mutation) do graphql_mutation(:job_play, variables, - <<-QL + <<-QL errors job { id @@ -28,7 +28,7 @@ RSpec.describe 'JobPlay', feature_category: :continuous_integration do } } } - QL + QL ) end @@ -63,7 +63,7 @@ RSpec.describe 'JobPlay', feature_category: :continuous_integration do } end - it 'provides those variables to the job', :aggregated_errors do + it 'provides those variables to the job', :aggregate_failures do expect_next_instance_of(Ci::PlayBuildService) do |instance| expect(instance).to receive(:execute).with(an_instance_of(Ci::Build), variables[:variables]).and_call_original end diff --git a/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb b/spec/requests/api/graphql/mutations/ci/job/retry_spec.rb index e49ee6f3163..4114c77491b 100644 --- a/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/job/retry_spec.rb @@ -16,12 +16,12 @@ RSpec.describe 'JobRetry', feature_category: :continuous_integration do id: job.to_global_id.to_s } graphql_mutation(:job_retry, variables, - <<-QL + <<-QL errors job { id } - QL + QL ) end @@ -57,12 +57,12 @@ RSpec.describe 'JobRetry', feature_category: :continuous_integration do } graphql_mutation(:job_retry, variables, - <<-QL + <<-QL errors job { id } - QL + QL ) end diff --git a/spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb b/spec/requests/api/graphql/mutations/ci/job/unschedule_spec.rb index 6868b0ea279..08e155e808b 100644 --- a/spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/job/unschedule_spec.rb @@ -15,7 +15,7 @@ RSpec.describe 'JobUnschedule', feature_category: :continuous_integration do id: job.to_global_id.to_s } graphql_mutation(:job_unschedule, variables, - <<-QL + <<-QL errors job { id diff --git a/spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb new file mode 100644 index 00000000000..4e25669a0ca --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'BulkDestroy', feature_category: :build_artifacts do + include GraphqlHelpers + + let(:maintainer) { create(:user) } + let(:developer) { create(:user) } + let(:first_artifact) { create(:ci_job_artifact) } + let(:second_artifact) { create(:ci_job_artifact, project: project) } + let(:second_artifact_another_project) { create(:ci_job_artifact) } + let(:project) { first_artifact.job.project } + let(:ids) { [first_artifact.to_global_id.to_s] } + let(:not_authorized_project_error_message) do + "The resource that you are attempting to access " \ + "does not exist or you don't have permission to perform this action" + end + + let(:mutation) do + variables = { + project_id: project.to_global_id.to_s, + ids: ids + } + graphql_mutation(:bulk_destroy_job_artifacts, variables, <<~FIELDS) + destroyedCount + destroyedIds + errors + FIELDS + end + + let(:mutation_response) { graphql_mutation_response(:bulk_destroy_job_artifacts) } + + it 'fails to destroy the artifact if a user not in a project' do + post_graphql_mutation(mutation, current_user: maintainer) + + expect(graphql_errors).to include( + a_hash_including('message' => not_authorized_project_error_message) + ) + + expect(first_artifact.reload).to be_persisted + end + + context 'when the `ci_job_artifact_bulk_destroy` feature flag is disabled' do + before do + stub_feature_flags(ci_job_artifact_bulk_destroy: false) + project.add_maintainer(maintainer) + end + + it 'returns a resource not available error' do + post_graphql_mutation(mutation, current_user: maintainer) + + expect(graphql_errors).to contain_exactly( + hash_including( + 'message' => '`ci_job_artifact_bulk_destroy` feature flag is disabled.' + ) + ) + end + end + + context "when the user is a developer in a project" do + before do + project.add_developer(developer) + end + + it 'fails to destroy the artifact' do + post_graphql_mutation(mutation, current_user: developer) + + expect(graphql_errors).to include( + a_hash_including('message' => not_authorized_project_error_message) + ) + + expect(response).to have_gitlab_http_status(:success) + expect(first_artifact.reload).to be_persisted + end + end + + context "when the user is a maintainer in a project" do + before do + project.add_maintainer(maintainer) + end + + shared_examples 'failing mutation' do + it 'rejects the request' do + post_graphql_mutation(mutation, current_user: maintainer) + + expect(graphql_errors(mutation_response)).to include(expected_error_message) + + expected_not_found_artifacts.each do |artifact| + expect { artifact.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + expected_found_artifacts.each do |artifact| + expect(artifact.reload).to be_persisted + end + end + end + + it 'destroys the artifact' do + post_graphql_mutation(mutation, current_user: maintainer) + + expect(mutation_response).to include("destroyedCount" => 1, "destroyedIds" => [gid_string(first_artifact)]) + expect(response).to have_gitlab_http_status(:success) + expect { first_artifact.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + context "and one artifact doesn't belong to the project" do + let(:not_owned_artifact) { create(:ci_job_artifact) } + let(:ids) { [first_artifact.to_global_id.to_s, not_owned_artifact.to_global_id.to_s] } + let(:expected_error_message) { "Not all artifacts belong to requested project" } + let(:expected_not_found_artifacts) { [] } + let(:expected_found_artifacts) { [first_artifact, not_owned_artifact] } + + it_behaves_like 'failing mutation' + end + + context "and multiple artifacts belong to the maintainer's project" do + let(:ids) { [first_artifact.to_global_id.to_s, second_artifact.to_global_id.to_s] } + + it 'destroys all artifacts' do + post_graphql_mutation(mutation, current_user: maintainer) + + expect(mutation_response).to include( + "destroyedCount" => 2, + "destroyedIds" => [gid_string(first_artifact), gid_string(second_artifact)] + ) + + expect(response).to have_gitlab_http_status(:success) + expect { first_artifact.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect { second_artifact.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context "and one artifact belongs to a different maintainer's project" do + let(:ids) { [first_artifact.to_global_id.to_s, second_artifact_another_project.to_global_id.to_s] } + let(:expected_found_artifacts) { [first_artifact, second_artifact_another_project] } + let(:expected_not_found_artifacts) { [] } + let(:expected_error_message) { "Not all artifacts belong to requested project" } + + it_behaves_like 'failing mutation' + end + + context "and not found" do + let(:ids) { [first_artifact.to_global_id.to_s, second_artifact.to_global_id.to_s] } + let(:not_found_ids) { expected_not_found_artifacts.map(&:id).join(',') } + let(:expected_error_message) { "Artifacts (#{not_found_ids}) not found" } + + before do + expected_not_found_artifacts.each(&:destroy!) + end + + context "with one artifact" do + let(:expected_not_found_artifacts) { [second_artifact] } + let(:expected_found_artifacts) { [first_artifact] } + + it_behaves_like 'failing mutation' + end + + context "with all artifact" do + let(:expected_not_found_artifacts) { [first_artifact, second_artifact] } + let(:expected_found_artifacts) { [] } + + it_behaves_like 'failing mutation' + end + end + + context 'when empty request' do + before do + project.add_maintainer(maintainer) + end + + context 'with nil value' do + let(:ids) { nil } + + it 'does nothing and returns empty answer' do + post_graphql_mutation(mutation, current_user: maintainer) + + expect_graphql_errors_to_include(/was provided invalid value for ids \(Expected value to not be null\)/) + end + end + + context 'with empty array' do + let(:ids) { [] } + + it 'raises argument error' do + post_graphql_mutation(mutation, current_user: maintainer) + + expect_graphql_errors_to_include(/IDs array of job artifacts can not be empty/) + end + end + end + + def gid_string(object) + Gitlab::GlobalId.build(object, id: object.id).to_s + end + end +end diff --git a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb index 55e728b2141..8791d793cb4 100644 --- a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb @@ -53,14 +53,29 @@ RSpec.describe 'CiJobTokenScopeAddProject', feature_category: :continuous_integr before do target_project.add_developer(current_user) + stub_feature_flags(frozen_outbound_job_token_scopes_override: false) end - it 'adds the target project to the job token scope' do + it 'adds the target project to the inbound job token scope' do expect do post_graphql_mutation(mutation, current_user: current_user) expect(response).to have_gitlab_http_status(:success) expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty - end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1) + end.to change { Ci::JobToken::ProjectScopeLink.inbound.count }.by(1) + end + + context 'when FF frozen_outbound_job_token_scopes is disabled' do + before do + stub_feature_flags(frozen_outbound_job_token_scopes: false) + end + + it 'adds the target project to the outbound job token scope' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty + end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1) + end end context 'when invalid target project is provided' do diff --git a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb index 99e55c44773..aa00069b241 100644 --- a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb +++ b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb @@ -5,6 +5,10 @@ require 'spec_helper' RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integration do include GraphqlHelpers + before do + stub_feature_flags(frozen_outbound_job_token_scopes_override: false) + end + let_it_be(:project) do create(:project, keep_latest_artifact: true, @@ -18,12 +22,11 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr full_path: project.full_path, keep_latest_artifact: false, job_token_scope_enabled: false, - inbound_job_token_scope_enabled: false, - opt_in_jwt: true + inbound_job_token_scope_enabled: false } end - let(:mutation) { graphql_mutation(:ci_cd_settings_update, variables) } + let(:mutation) { graphql_mutation(:project_ci_cd_settings_update, variables) } context 'when unauthorized' do let(:user) { create(:user) } @@ -61,7 +64,36 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr expect(project.keep_latest_artifact).to eq(false) end - it 'updates job_token_scope_enabled' do + describe 'ci_cd_settings_update deprecated mutation' do + let(:mutation) { graphql_mutation(:ci_cd_settings_update, variables) } + + it 'returns error' do + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_errors).to( + include( + hash_including('message' => '`remove_cicd_settings_update` feature flag is enabled.') + ) + ) + end + + context 'when remove_cicd_settings_update FF is disabled' do + before do + stub_feature_flags(remove_cicd_settings_update: false) + end + + it 'updates ci cd settings' do + post_graphql_mutation(mutation, current_user: user) + + project.reload + + expect(response).to have_gitlab_http_status(:success) + expect(project.keep_latest_artifact).to eq(false) + end + end + end + + it 'allows setting job_token_scope_enabled to false' do post_graphql_mutation(mutation, current_user: user) project.reload @@ -70,6 +102,50 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr expect(project.ci_outbound_job_token_scope_enabled).to eq(false) end + context 'when job_token_scope_enabled: true' do + let(:variables) do + { + full_path: project.full_path, + keep_latest_artifact: false, + job_token_scope_enabled: true, + inbound_job_token_scope_enabled: false + } + end + + it 'prevents the update', :aggregate_failures do + project.update!(ci_outbound_job_token_scope_enabled: false) + post_graphql_mutation(mutation, current_user: user) + + project.reload + + expect(response).to have_gitlab_http_status(:success) + expect(graphql_errors).to( + include( + hash_including( + 'message' => 'job_token_scope_enabled can only be set to false' + ) + ) + ) + expect(project.ci_outbound_job_token_scope_enabled).to eq(false) + end + end + + context 'when FF frozen_outbound_job_token_scopes is disabled' do + before do + stub_feature_flags(frozen_outbound_job_token_scopes: false) + end + + it 'allows setting job_token_scope_enabled to true' do + project.update!(ci_outbound_job_token_scope_enabled: true) + post_graphql_mutation(mutation, current_user: user) + + project.reload + + expect(response).to have_gitlab_http_status(:success) + expect(project.ci_outbound_job_token_scope_enabled).to eq(false) + end + end + it 'does not update job_token_scope_enabled if not specified' do variables.except!(:job_token_scope_enabled) @@ -101,30 +177,6 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr expect(response).to have_gitlab_http_status(:success) expect(project.ci_inbound_job_token_scope_enabled).to eq(true) end - - context 'when ci_inbound_job_token_scope disabled' do - before do - stub_feature_flags(ci_inbound_job_token_scope: false) - end - - it 'does not update inbound_job_token_scope_enabled' do - post_graphql_mutation(mutation, current_user: user) - - project.reload - - expect(response).to have_gitlab_http_status(:success) - expect(project.ci_inbound_job_token_scope_enabled).to eq(true) - end - end - end - - it 'updates ci_opt_in_jwt' do - post_graphql_mutation(mutation, current_user: user) - - project.reload - - expect(response).to have_gitlab_http_status(:success) - expect(project.ci_opt_in_jwt).to eq(true) end context 'when bad arguments are provided' do diff --git a/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb b/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb new file mode 100644 index 00000000000..1658c277ed0 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb @@ -0,0 +1,313 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:group_owner) { create(:user) } + let_it_be(:admin) { create(:admin) } + + let_it_be(:group) { create(:group) } + let_it_be(:other_group) { create(:group) } + + let(:mutation_params) do + { + description: 'create description', + maintenance_note: 'create maintenance note', + maximum_timeout: 900, + access_level: 'REF_PROTECTED', + paused: true, + run_untagged: false, + tag_list: %w[tag1 tag2] + }.deep_merge(mutation_scope_params) + end + + let(:mutation) do + variables = { + **mutation_params + } + + graphql_mutation( + :runner_create, + variables, + <<-QL + runner { + ephemeralAuthenticationToken + + runnerType + description + maintenanceNote + paused + tagList + accessLevel + locked + maximumTimeout + runUntagged + } + errors + QL + ) + end + + let(:mutation_response) { graphql_mutation_response(:runner_create) } + + before do + group.add_owner(group_owner) + end + + shared_context 'when model is invalid returns error' do + let(:mutation_params) do + { + description: '', + maintenanceNote: '', + paused: true, + accessLevel: 'NOT_PROTECTED', + runUntagged: false, + tagList: [], + maximumTimeout: 1 + }.deep_merge(mutation_scope_params) + end + + it do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + + expect(mutation_response['errors']).to contain_exactly( + 'Tags list can not be empty when runner is not allowed to pick untagged jobs', + 'Maximum timeout needs to be at least 10 minutes' + ) + end + end + + shared_context 'when user does not have permissions' do + let(:current_user) { user } + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect_graphql_errors_to_include( + 'The resource that you are attempting to access does not exist ' \ + "or you don't have permission to perform this action" + ) + end + end + + shared_context 'when :create_runner_workflow_for_namespace feature flag is disabled' do + before do + stub_feature_flags(create_runner_workflow_for_namespace: [other_group]) + end + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect_graphql_errors_to_include('`create_runner_workflow_for_namespace` feature flag is disabled.') + end + end + + shared_examples 'when runner is created successfully' do + it do + expected_args = { user: current_user, params: anything } + expect_next_instance_of(::Ci::Runners::CreateRunnerService, expected_args) do |service| + expect(service).to receive(:execute).and_call_original + end + + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + + expect(mutation_response['errors']).to eq([]) + expect(mutation_response['runner']).not_to be_nil + mutation_params.except(:group_id, :project_id).each_key do |key| + expect(mutation_response['runner'][key.to_s.camelize(:lower)]).to eq mutation_params[key] + end + + expect(mutation_response['runner']['ephemeralAuthenticationToken']) + .to start_with Ci::Runner::CREATED_RUNNER_TOKEN_PREFIX + end + end + + context 'when runnerType is INSTANCE_TYPE' do + let(:mutation_scope_params) do + { runner_type: 'INSTANCE_TYPE' } + end + + it_behaves_like 'when user does not have permissions' + + context 'when user has permissions', :enable_admin_mode do + let(:current_user) { admin } + + context 'when :create_runner_workflow_for_admin feature flag is disabled' do + before do + stub_feature_flags(create_runner_workflow_for_admin: false) + end + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect_graphql_errors_to_include('`create_runner_workflow_for_admin` feature flag is disabled.') + end + end + + it_behaves_like 'when runner is created successfully' + it_behaves_like 'when model is invalid returns error' + end + end + + context 'when runnerType is GROUP_TYPE' do + let(:mutation_scope_params) do + { + runner_type: 'GROUP_TYPE', + group_id: group.to_global_id + } + end + + before do + stub_feature_flags(create_runner_workflow_for_namespace: [group]) + end + + it_behaves_like 'when user does not have permissions' + + context 'when user has permissions' do + context 'when user is group owner' do + let(:current_user) { group_owner } + + it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled' + it_behaves_like 'when runner is created successfully' + it_behaves_like 'when model is invalid returns error' + + context 'when group_id is missing' do + let(:mutation_scope_params) do + { runner_type: 'GROUP_TYPE' } + end + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect_graphql_errors_to_include('`group_id` is missing') + end + end + + context 'when group_id is malformed' do + let(:mutation_scope_params) do + { + runner_type: 'GROUP_TYPE', + group_id: '' + } + end + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect_graphql_errors_to_include( + "RunnerCreateInput! was provided invalid value for groupId" + ) + end + end + + context 'when group_id does not exist' do + let(:mutation_scope_params) do + { + runner_type: 'GROUP_TYPE', + group_id: "gid://gitlab/Group/#{non_existing_record_id}" + } + end + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(flattened_errors).not_to be_empty + end + end + end + + context 'when user is admin in admin mode', :enable_admin_mode do + let(:current_user) { admin } + + it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled' + it_behaves_like 'when runner is created successfully' + it_behaves_like 'when model is invalid returns error' + end + end + end + + context 'when runnerType is PROJECT_TYPE' do + let_it_be(:project) { create(:project, namespace: group) } + + let(:mutation_scope_params) do + { + runner_type: 'PROJECT_TYPE', + project_id: project.to_global_id + } + end + + it_behaves_like 'when user does not have permissions' + + context 'when user has permissions' do + context 'when user is group owner' do + let(:current_user) { group_owner } + + it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled' + it_behaves_like 'when runner is created successfully' + it_behaves_like 'when model is invalid returns error' + + context 'when project_id is missing' do + let(:mutation_scope_params) do + { runner_type: 'PROJECT_TYPE' } + end + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect_graphql_errors_to_include('`project_id` is missing') + end + end + + context 'when project_id is malformed' do + let(:mutation_scope_params) do + { + runner_type: 'PROJECT_TYPE', + project_id: '' + } + end + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect_graphql_errors_to_include( + "RunnerCreateInput! was provided invalid value for projectId" + ) + end + end + + context 'when project_id does not exist' do + let(:mutation_scope_params) do + { + runner_type: 'PROJECT_TYPE', + project_id: "gid://gitlab/Project/#{non_existing_record_id}" + } + end + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect_graphql_errors_to_include( + 'The resource that you are attempting to access does not exist ' \ + "or you don't have permission to perform this action" + ) + end + end + end + + context 'when user is admin in admin mode', :enable_admin_mode do + let(:current_user) { admin } + + it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled' + it_behaves_like 'when runner is created successfully' + it_behaves_like 'when model is invalid returns error' + end + end + end +end 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 index f544cef8864..ef0d44395bf 100644 --- 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 @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Create a new cluster agent token', feature_category: :kubernetes_management do +RSpec.describe 'Create a new cluster agent token', feature_category: :deployment_management do include GraphqlHelpers let_it_be(:cluster_agent) { create(:cluster_agent) } diff --git a/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb b/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb index 66e6c5cc629..1d1e72dcff9 100644 --- a/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb +++ b/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Create a new cluster agent', feature_category: :kubernetes_management do +RSpec.describe 'Create a new cluster agent', feature_category: :deployment_management do include GraphqlHelpers let(:project) { create(:project, :public, :repository) } diff --git a/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb b/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb index 27a566dfb8c..b70a6282a7a 100644 --- a/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb +++ b/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Delete a cluster agent', feature_category: :kubernetes_management do +RSpec.describe 'Delete a cluster agent', feature_category: :deployment_management do include GraphqlHelpers let(:cluster_agent) { create(:cluster_agent) } diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb index 8b76c19cda6..ef159e41d3d 100644 --- a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb @@ -39,7 +39,7 @@ RSpec.describe 'Destroying a container repository', feature_category: :container expect(DeleteContainerRepositoryWorker) .not_to receive(:perform_async) - expect { subject }.to change { ::Packages::Event.count }.by(1) + subject expect(container_repository_mutation_response).to match_schema('graphql/container_repository') expect(container_repository_mutation_response['status']).to eq('DELETE_SCHEDULED') @@ -53,7 +53,7 @@ RSpec.describe 'Destroying a container repository', feature_category: :container expect(DeleteContainerRepositoryWorker) .not_to receive(:perform_async).with(user.id, container_repository.id) - expect { subject }.not_to change { ::Packages::Event.count } + subject expect(mutation_response).to be_nil end diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb index 9e07a831076..0cb607e13ec 100644 --- a/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb +++ b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb @@ -36,7 +36,7 @@ RSpec.describe 'Destroying a container repository tags', feature_category: :cont it 'destroys the container repository tags' do expect(Projects::ContainerRepository::DeleteTagsService) .to receive(:new).and_call_original - expect { subject }.to change { ::Packages::Event.count }.by(1) + subject expect(tag_names_response).to eq(tags) expect(errors_response).to eq([]) @@ -50,7 +50,7 @@ RSpec.describe 'Destroying a container repository tags', feature_category: :cont expect(Projects::ContainerRepository::DeleteTagsService) .not_to receive(:new) - expect { subject }.not_to change { ::Packages::Event.count } + subject expect(mutation_response).to be_nil end @@ -89,7 +89,7 @@ RSpec.describe 'Destroying a container repository tags', feature_category: :cont let(:tags) { Array.new(Mutations::ContainerRepositories::DestroyTags::LIMIT + 1, 'x') } it 'returns too many tags error' do - expect { subject }.not_to change { ::Packages::Event.count } + subject explanation = graphql_errors.dig(0, 'message') expect(explanation).to eq(Mutations::ContainerRepositories::DestroyTags::TOO_MANY_TAGS_ERROR_MESSAGE) @@ -113,7 +113,7 @@ RSpec.describe 'Destroying a container repository tags', feature_category: :cont it 'does not create a package event' do expect(::Packages::CreateEventService).not_to receive(:new) - expect { subject }.not_to change { ::Packages::Event.count } + subject end end end diff --git a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb index ea2ce8a13e2..19a52086f34 100644 --- a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb +++ b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Creation of a new Custom Emoji', feature_category: :not_owned do +RSpec.describe 'Creation of a new Custom Emoji', feature_category: :shared do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb index ad7a043909a..2623d3d8410 100644 --- a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Deletion of custom emoji', feature_category: :not_owned do +RSpec.describe 'Deletion of custom emoji', feature_category: :shared do include GraphqlHelpers let_it_be(:group) { create(:group) } diff --git a/spec/requests/api/graphql/mutations/design_management/update_spec.rb b/spec/requests/api/graphql/mutations/design_management/update_spec.rb new file mode 100644 index 00000000000..9558f2538f1 --- /dev/null +++ b/spec/requests/api/graphql/mutations/design_management/update_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "updating designs", feature_category: :design_management do + include GraphqlHelpers + include DesignManagementTestHelpers + + let_it_be(:issue) { create(:issue) } + let_it_be_with_reload(:design) { create(:design, description: 'old description', issue: issue) } + let_it_be(:developer) { create(:user, developer_projects: [issue.project]) } + + let(:user) { developer } + let(:description) { 'new description' } + + let(:mutation) do + input = { + id: design.to_global_id.to_s, + description: description + }.compact + + graphql_mutation(:design_management_update, input, <<~FIELDS) + errors + design { + description + descriptionHtml + } + FIELDS + end + + let(:update_design) { post_graphql_mutation(mutation, current_user: user) } + let(:mutation_response) { graphql_mutation_response(:design_management_update) } + + before do + enable_design_management + end + + it 'updates design' do + update_design + + expect(graphql_errors).not_to be_present + expect(mutation_response).to eq( + 'errors' => [], + 'design' => { + 'description' => description, + 'descriptionHtml' => "<p data-sourcepos=\"1:1-1:15\" dir=\"auto\">#{description}</p>" + } + ) + end + + context 'when the user is not allowed to update designs' do + let(:user) { create(:user) } + + it 'returns an error' do + update_design + + expect(graphql_errors).to be_present + end + end + + context 'when update fails' do + let(:description) { 'x' * 1_000_001 } + + it 'returns an error' do + update_design + + expect(graphql_errors).not_to be_present + expect(mutation_response).to eq( + 'errors' => ["Description is too long (maximum is 1000000 characters)"], + 'design' => { + 'description' => 'old description', + 'descriptionHtml' => '<p data-sourcepos="1:1-1:15" dir="auto">old description</p>' + } + ) + end + end +end diff --git a/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb b/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb index b9c83311908..b729585a89b 100644 --- a/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb @@ -8,7 +8,9 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do let_it_be(:developer) { create(:user) } let_it_be(:group) { create(:group).tap { |group| group.add_developer(developer) } } let_it_be(:project) { create(:project, group: group) } - let_it_be(:updatable_issues, reload: true) { create_list(:issue, 2, project: project) } + let_it_be(:label1) { create(:group_label, group: group) } + let_it_be(:label2) { create(:group_label, group: group) } + let_it_be(:updatable_issues, reload: true) { create_list(:issue, 2, project: project, label_ids: [label1.id]) } let_it_be(:milestone) { create(:milestone, group: group) } let(:parent) { project } @@ -21,10 +23,36 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do let(:additional_arguments) do { assignee_ids: [current_user.to_gid.to_s], - milestone_id: milestone.to_gid.to_s + milestone_id: milestone.to_gid.to_s, + state_event: :CLOSE, + add_label_ids: [label2.to_gid.to_s], + remove_label_ids: [label1.to_gid.to_s], + subscription_event: :UNSUBSCRIBE } end + before_all do + updatable_issues.each { |i| i.subscribe(developer, project) } + end + + context 'when Gitlab is FOSS only' do + unless Gitlab.ee? + context 'when parent is a group' do + let(:parent) { group } + + it 'does not allow bulk updating issues at the group level' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_errors).to contain_exactly( + hash_including( + 'message' => match(/does not represent an instance of IssueParent/) + ) + ) + end + end + end + end + context 'when the `bulk_update_issues_mutation` feature flag is disabled' do before do stub_feature_flags(bulk_update_issues_mutation: false) @@ -67,6 +95,11 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do updatable_issues.each(&:reload) end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([]).to([current_user.id] * 2) .and(change { updatable_issues.map(&:milestone_id) }.from([nil] * 2).to([milestone.id] * 2)) + .and(change { updatable_issues.map(&:state) }.from(['opened'] * 2).to(['closed'] * 2)) + .and(change { updatable_issues.flat_map(&:label_ids) }.from([label1.id] * 2).to([label2.id] * 2)) + .and( + change { updatable_issues.map { |i| i.subscribed?(developer, project) } }.from([true] * 2).to([false] * 2) + ) expect(mutation_response).to include( 'updatedIssueCount' => updatable_issues.count @@ -88,37 +121,6 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do end end - context 'when scoping to a parent group' do - let(:parent) { group } - - it 'updates all issues' do - expect do - post_graphql_mutation(mutation, current_user: current_user) - updatable_issues.each(&:reload) - end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([]).to([current_user.id] * 2) - .and(change { updatable_issues.map(&:milestone_id) }.from([nil] * 2).to([milestone.id] * 2)) - - expect(mutation_response).to include( - 'updatedIssueCount' => updatable_issues.count - ) - end - - context 'when current user cannot read the specified group' do - let(:parent) { create(:group, :private) } - - it 'returns a resource not found error' do - post_graphql_mutation(mutation, current_user: current_user) - - expect(graphql_errors).to contain_exactly( - hash_including( - 'message' => "The resource that you are attempting to access does not exist or you don't have " \ - 'permission to perform this action' - ) - ) - end - end - end - context 'when setting arguments to null or none' do let(:additional_arguments) { { assignee_ids: [], milestone_id: nil } } diff --git a/spec/requests/api/graphql/mutations/issues/create_spec.rb b/spec/requests/api/graphql/mutations/issues/create_spec.rb index d2d2f0014d6..b5a9c549045 100644 --- a/spec/requests/api/graphql/mutations/issues/create_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/create_spec.rb @@ -66,7 +66,6 @@ RSpec.describe 'Create an issue', feature_category: :team_planning do created_issue = Issue.last expect(created_issue.work_item_type.base_type).to eq('task') - expect(created_issue.issue_type).to eq('task') end end diff --git a/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb b/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb index ad70129a7bc..f15b52f53a3 100644 --- a/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb +++ b/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb @@ -5,126 +5,14 @@ require 'spec_helper' RSpec.describe 'GroupMemberBulkUpdate', feature_category: :subgroups do include GraphqlHelpers - let_it_be(:current_user) { create(:user) } - let_it_be(:user1) { create(:user) } - let_it_be(:user2) { create(:user) } - let_it_be(:group) { create(:group) } - let_it_be(:group_member1) { create(:group_member, group: group, user: user1) } - let_it_be(:group_member2) { create(:group_member, group: group, user: user2) } + let_it_be(:parent_group) { create(:group) } + let_it_be(:parent_group_member) { create(:group_member, group: parent_group) } + let_it_be(:group) { create(:group, parent: parent_group) } + let_it_be(:source) { group } + let_it_be(:member_type) { :group_member } let_it_be(:mutation_name) { :group_member_bulk_update } + let_it_be(:source_id_key) { 'group_id' } + let_it_be(:response_member_field) { 'groupMembers' } - let(:input) do - { - 'group_id' => group.to_global_id.to_s, - 'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s], - 'access_level' => 'GUEST' - } - end - - let(:extra_params) { { expires_at: 10.days.from_now } } - let(:input_params) { input.merge(extra_params) } - let(:mutation) { graphql_mutation(mutation_name, input_params) } - let(:mutation_response) { graphql_mutation_response(mutation_name) } - - context 'when user is not logged-in' do - it_behaves_like 'a mutation that returns a top-level access error' - end - - context 'when user is not an owner' do - before do - group.add_maintainer(current_user) - end - - it_behaves_like 'a mutation that returns a top-level access error' - end - - context 'when user is an owner' do - before do - group.add_owner(current_user) - end - - shared_examples 'updates the user access role' do - specify do - post_graphql_mutation(mutation, current_user: current_user) - - new_access_levels = mutation_response['groupMembers'].map { |member| member['accessLevel']['integerValue'] } - expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['errors']).to be_empty - expect(new_access_levels).to all(be Gitlab::Access::GUEST) - end - end - - it_behaves_like 'updates the user access role' - - context 'when inherited members are passed' do - let_it_be(:subgroup) { create(:group, parent: group) } - let_it_be(:subgroup_member) { create(:group_member, group: subgroup) } - - let(:input) do - { - 'group_id' => group.to_global_id.to_s, - 'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s, subgroup_member.user.to_global_id.to_s], - 'access_level' => 'GUEST' - } - end - - it 'does not update the members' do - post_graphql_mutation(mutation, current_user: current_user) - - error = Mutations::Members::Groups::BulkUpdate::INVALID_MEMBERS_ERROR - expect(json_response['errors'].first['message']).to include(error) - end - end - - context 'when members count is more than the allowed limit' do - let(:max_members_update_limit) { 1 } - - before do - stub_const('Mutations::Members::Groups::BulkUpdate::MAX_MEMBERS_UPDATE_LIMIT', max_members_update_limit) - end - - it 'does not update the members' do - post_graphql_mutation(mutation, current_user: current_user) - - error = Mutations::Members::Groups::BulkUpdate::MAX_MEMBERS_UPDATE_ERROR - expect(json_response['errors'].first['message']).to include(error) - end - end - - context 'when the update service raises access denied error' do - before do - allow_next_instance_of(Members::UpdateService) do |instance| - allow(instance).to receive(:execute).and_raise(Gitlab::Access::AccessDeniedError) - end - end - - it 'does not update the members' do - post_graphql_mutation(mutation, current_user: current_user) - - expect(mutation_response['groupMembers']).to be_nil - expect(mutation_response['errors']) - .to contain_exactly("Unable to update members, please check user permissions.") - end - end - - context 'when the update service returns an error message' do - before do - allow_next_instance_of(Members::UpdateService) do |instance| - error_result = { - message: 'Expires at cannot be a date in the past', - status: :error, - members: [group_member1] - } - allow(instance).to receive(:execute).and_return(error_result) - end - end - - it 'will pass through the error' do - post_graphql_mutation(mutation, current_user: current_user) - - expect(mutation_response['groupMembers'].first['id']).to eq(group_member1.to_global_id.to_s) - expect(mutation_response['errors']).to contain_exactly('Expires at cannot be a date in the past') - end - end - end + it_behaves_like 'members bulk update mutation' end diff --git a/spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb b/spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb new file mode 100644 index 00000000000..cbef9715cbe --- /dev/null +++ b/spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'ProjectMemberBulkUpdate', feature_category: :projects do + include GraphqlHelpers + + let_it_be(:parent_group) { create(:group) } + let_it_be(:parent_group_member) { create(:group_member, group: parent_group) } + let_it_be(:project) { create(:project, group: parent_group) } + let_it_be(:source) { project } + let_it_be(:member_type) { :project_member } + let_it_be(:mutation_name) { :project_member_bulk_update } + let_it_be(:source_id_key) { 'project_id' } + let_it_be(:response_member_field) { 'projectMembers' } + + it_behaves_like 'members bulk update mutation' +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 b5f2042c42a..d41628704a1 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 @@ -106,7 +106,7 @@ RSpec.describe 'Setting assignees of a merge request', :assume_throttled, featur end context 'when passing an empty list of assignees' do - let(:db_query_limit) { 31 } + let(:db_query_limit) { 35 } let(:input) { { assignee_usernames: [] } } before do diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb index bce57b47aab..d81744abe1b 100644 --- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb +++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb @@ -19,7 +19,11 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create, feature_categ graphql_mutation_response(:create_annotation) end - specify { expect(described_class).to require_graphql_authorizations(:create_metrics_dashboard_annotation) } + before do + stub_feature_flags(remove_monitor_metrics: false) + end + + specify { expect(described_class).to require_graphql_authorizations(:admin_metrics_dashboard_annotation) } context 'when annotation source is environment' do let(:mutation) do @@ -103,6 +107,15 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create, feature_categ it_behaves_like 'an invalid argument to the mutation', argument_name: :environment_id end + + context 'when metrics dashboard feature is unavailable' do + before do + stub_feature_flags(remove_monitor_metrics: true) + end + + it_behaves_like 'a mutation that returns top-level errors', + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + end end end diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb index f505dc25dc0..09977cd19d7 100644 --- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb +++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb @@ -17,7 +17,11 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete, feature_categ graphql_mutation_response(:delete_annotation) end - specify { expect(described_class).to require_graphql_authorizations(:delete_metrics_dashboard_annotation) } + before do + stub_feature_flags(remove_monitor_metrics: false) + end + + specify { expect(described_class).to require_graphql_authorizations(:admin_metrics_dashboard_annotation) } context 'when the user has permission to delete the annotation' do before do @@ -54,6 +58,15 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete, feature_categ expect(mutation_response['errors']).to eq([service_response[:message]]) end end + + context 'when metrics dashboard feature is unavailable' do + before do + stub_feature_flags(remove_monitor_metrics: true) + end + + it_behaves_like 'a mutation that returns top-level errors', + errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] + end end context 'when the user does not have permission to delete the annotation' do diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb index a6253ba424b..e6feba059c4 100644 --- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb @@ -104,7 +104,8 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do end context 'as work item' do - let(:noteable) { create(:work_item, :issue, project: project) } + let_it_be(:project) { create(:project) } + let_it_be(:noteable) { create(:work_item, :issue, project: project) } context 'when using internal param' do let(:variables_extra) { { internal: true } } @@ -130,6 +131,20 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do it_behaves_like 'a mutation that returns top-level errors', errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] end + + context 'when body contains quick actions' do + let_it_be(:noteable) { create(:work_item, :task, project: project) } + + let(:variables_extra) { {} } + + it_behaves_like 'work item supports labels widget updates via quick actions' + it_behaves_like 'work item does not support labels widget updates via quick actions' + it_behaves_like 'work item supports assignee widget updates via quick actions' + it_behaves_like 'work item does not support assignee widget updates via quick actions' + it_behaves_like 'work item supports start and due date widget updates via quick actions' + it_behaves_like 'work item does not support start and due date widget updates via quick actions' + it_behaves_like 'work item supports type change via quick actions' + end end end diff --git a/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb b/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb new file mode 100644 index 00000000000..c5dc6f390d9 --- /dev/null +++ b/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe "Sync project fork", feature_category: :source_code_management do + include GraphqlHelpers + include ProjectForksHelper + include ExclusiveLeaseHelpers + + let_it_be(:source_project) { create(:project, :repository, :public) } + let_it_be(:current_user) { create(:user, maintainer_projects: [source_project]) } + let_it_be(:project, refind: true) { fork_project(source_project, current_user, { repository: true }) } + let_it_be(:target_branch) { project.default_branch } + + let(:mutation) do + params = { project_path: project.full_path, target_branch: target_branch } + + graphql_mutation(:project_sync_fork, params) do + <<-QL.strip_heredoc + details { + ahead + behind + isSyncing + hasConflicts + } + errors + QL + end + end + + before do + source_project.change_head('feature') + end + + context 'when synchronize_fork feature flag is disabled' do + before do + stub_feature_flags(synchronize_fork: false) + end + + it 'does not call the sync service' do + expect(::Projects::Forks::SyncWorker).not_to receive(:perform_async) + + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_mutation_response(:project_sync_fork)).to eq( + { + 'details' => nil, + 'errors' => ['Feature flag is disabled'] + }) + end + end + + context 'when the branch is protected', :use_clean_rails_redis_caching do + let_it_be(:protected_branch) do + create(:protected_branch, :no_one_can_push, project: project, name: target_branch) + end + + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not call the sync service' do + expect(::Projects::Forks::SyncWorker).not_to receive(:perform_async) + + post_graphql_mutation(mutation, current_user: current_user) + end + end + + context 'when the user does not have permission' do + let_it_be(:current_user) { create(:user) } + + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not call the sync service' do + expect(::Projects::Forks::SyncWorker).not_to receive(:perform_async) + + post_graphql_mutation(mutation, current_user: current_user) + end + end + + context 'when the user has permission' do + context 'and the sync service executes successfully', :sidekiq_inline do + it 'calls the sync service' do + expect(::Projects::Forks::SyncWorker).to receive(:perform_async).and_call_original + + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_mutation_response(:project_sync_fork)).to eq( + { + 'details' => { 'ahead' => 30, 'behind' => 0, "hasConflicts" => false, "isSyncing" => false }, + 'errors' => [] + }) + end + end + + context 'and the sync service fails to execute' do + let(:target_branch) { 'markdown' } + + def expect_error_response(message) + expect(::Projects::Forks::SyncWorker).not_to receive(:perform_async) + + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_mutation_response(:project_sync_fork)['errors']).to eq([message]) + end + + context 'when fork details cannot be resolved' do + let_it_be(:project) { source_project } + + it 'returns an error' do + expect_error_response('This branch of this project cannot be updated from the upstream') + end + end + + context 'when the specified branch does not exist' do + let(:target_branch) { 'non-existent-branch' } + + it 'returns an error' do + expect_error_response('Target branch does not exist') + end + end + + context 'when the previous execution resulted in a conflict' do + it 'returns an error' do + expect_next_instance_of(::Projects::Forks::Details) do |instance| + expect(instance).to receive(:has_conflicts?).twice.and_return(true) + end + + expect_error_response('The synchronization cannot happen due to the merge conflict') + expect(graphql_mutation_response(:project_sync_fork)['details']['hasConflicts']).to eq(true) + end + end + + context 'when the request is rate limited' do + it 'returns an error' do + expect(Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true) + + expect_error_response('This service has been called too many times.') + end + end + + context 'when another fork sync is in progress' do + it 'returns an error' do + expect_next_instance_of(Projects::Forks::Details) do |instance| + lease = instance_double(Gitlab::ExclusiveLease, try_obtain: false, exists?: true) + expect(instance).to receive(:exclusive_lease).twice.and_return(lease) + end + + expect_error_response('Another fork sync is already in progress') + expect(graphql_mutation_response(:project_sync_fork)['details']['isSyncing']).to eq(true) + end + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb b/spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb index 418a0e47a36..311ff48a846 100644 --- a/spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb +++ b/spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb @@ -32,7 +32,6 @@ RSpec.describe 'Creation of a new release asset link', feature_category: :releas url linkType directAssetUrl - external } errors FIELDS @@ -49,8 +48,7 @@ RSpec.describe 'Creation of a new release asset link', feature_category: :releas name: mutation_arguments[:name], url: mutation_arguments[:url], linkType: mutation_arguments[:linkType], - directAssetUrl: end_with(mutation_arguments[:directAssetPath]), - external: true + directAssetUrl: end_with(mutation_arguments[:directAssetPath]) }.with_indifferent_access expect(mutation_response[:link]).to include(expected_response) diff --git a/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb b/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb index b6d2c3f691d..cda1030c6d6 100644 --- a/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb +++ b/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb @@ -22,7 +22,6 @@ RSpec.describe 'Deletes a release asset link', feature_category: :release_orches url linkType directAssetUrl - external } errors FIELDS @@ -39,8 +38,7 @@ RSpec.describe 'Deletes a release asset link', feature_category: :release_orches name: release_link.name, url: release_link.url, linkType: release_link.link_type.upcase, - directAssetUrl: end_with(release_link.filepath), - external: true + directAssetUrl: end_with(release_link.filepath) }.with_indifferent_access expect(mutation_response[:link]).to match(expected_response) diff --git a/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb b/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb index 61395cc4042..45028cba3ae 100644 --- a/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb +++ b/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb @@ -40,7 +40,6 @@ RSpec.describe 'Updating an existing release asset link', feature_category: :rel url linkType directAssetUrl - external } errors FIELDS @@ -57,8 +56,7 @@ RSpec.describe 'Updating an existing release asset link', feature_category: :rel name: mutation_arguments[:name], url: mutation_arguments[:url], linkType: mutation_arguments[:linkType], - directAssetUrl: end_with(mutation_arguments[:directAssetPath]), - external: true + directAssetUrl: end_with(mutation_arguments[:directAssetPath]) }.with_indifferent_access expect(mutation_response[:link]).to include(expected_response) diff --git a/spec/requests/api/graphql/mutations/releases/create_spec.rb b/spec/requests/api/graphql/mutations/releases/create_spec.rb index 295b8c0e97e..7cb421f17a3 100644 --- a/spec/requests/api/graphql/mutations/releases/create_spec.rb +++ b/spec/requests/api/graphql/mutations/releases/create_spec.rb @@ -59,7 +59,6 @@ RSpec.describe 'Creation of a new release', feature_category: :release_orchestra name url linkType - external directAssetUrl } } @@ -135,7 +134,6 @@ RSpec.describe 'Creation of a new release', feature_category: :release_orchestra name: asset_link[:name], url: asset_link[:url], linkType: asset_link[:linkType], - external: true, directAssetUrl: expected_direct_asset_url }] } diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb index fa087e6773c..3b98ee3c2e9 100644 --- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb @@ -193,7 +193,6 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d end it_behaves_like 'Snowplow event tracking with RedisHLL context' do - let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } let(:user) { current_user } let(:property) { 'g_edit_by_snippet_ide' } let(:namespace) { project.namespace } @@ -203,8 +202,6 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d let(:context) do [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event_name).to_context] end - - let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } end end end diff --git a/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb index 967ad75c906..65b8083c74f 100644 --- a/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb +++ b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb @@ -11,7 +11,8 @@ RSpec.describe Mutations::UserPreferences::Update, feature_category: :user_profi let(:input) do { - 'issuesSort' => sort_value + 'issuesSort' => sort_value, + 'visibilityPipelineIdType' => 'IID' } end @@ -24,15 +25,20 @@ RSpec.describe Mutations::UserPreferences::Update, feature_category: :user_profi expect(response).to have_gitlab_http_status(:success) expect(mutation_response['userPreferences']['issuesSort']).to eq(sort_value) + expect(mutation_response['userPreferences']['visibilityPipelineIdType']).to eq('IID') expect(current_user.user_preference.persisted?).to eq(true) expect(current_user.user_preference.issues_sort).to eq(Types::IssueSortEnum.values[sort_value].value.to_s) + expect(current_user.user_preference.visibility_pipeline_id_type).to eq('iid') end end context 'when user has existing preference' do before do - current_user.create_user_preference!(issues_sort: Types::IssueSortEnum.values['TITLE_DESC'].value) + current_user.create_user_preference!( + issues_sort: Types::IssueSortEnum.values['TITLE_DESC'].value, + visibility_pipeline_id_type: 'id' + ) end it 'updates the existing value' do @@ -42,8 +48,10 @@ RSpec.describe Mutations::UserPreferences::Update, feature_category: :user_profi expect(response).to have_gitlab_http_status(:success) expect(mutation_response['userPreferences']['issuesSort']).to eq(sort_value) + expect(mutation_response['userPreferences']['visibilityPipelineIdType']).to eq('IID') expect(current_user.user_preference.issues_sort).to eq(Types::IssueSortEnum.values[sort_value].value.to_s) + expect(current_user.user_preference.visibility_pipeline_id_type).to eq('iid') end end end diff --git a/spec/requests/api/graphql/mutations/work_items/convert_spec.rb b/spec/requests/api/graphql/mutations/work_items/convert_spec.rb new file mode 100644 index 00000000000..97289597331 --- /dev/null +++ b/spec/requests/api/graphql/mutations/work_items/convert_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe "Converts a work item to a new type", feature_category: :team_planning do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } } + let_it_be(:new_type) { create(:work_item_type, :incident, :default) } + let_it_be(:work_item, refind: true) do + create(:work_item, :task, project: project, milestone: create(:milestone, project: project)) + end + + let(:work_item_type_id) { new_type.to_global_id.to_s } + let(:mutation) { graphql_mutation(:workItemConvert, input) } + let(:mutation_response) { graphql_mutation_response(:work_item_convert) } + let(:input) do + { + 'id' => work_item.to_global_id.to_s, + 'work_item_type_id' => work_item_type_id + } + end + + context 'when user is not allowed to update a work item' do + let(:current_user) { create(:user) } + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user has permissions to convert the work item type' do + let(:current_user) { developer } + + context 'when work item type does not exist' do + let(:work_item_type_id) { "gid://gitlab/WorkItems::Type/#{non_existing_record_id}" } + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_errors).to include( + a_hash_including('message' => "Work Item type with id #{non_existing_record_id} was not found") + ) + end + end + + it 'converts the work item', :aggregate_failures do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change { work_item.reload.work_item_type }.to(new_type) + + expect(response).to have_gitlab_http_status(:success) + expect(work_item.reload.work_item_type.base_type).to eq('incident') + expect(mutation_response['workItem']).to include('id' => work_item.to_global_id.to_s) + expect(work_item.reload.milestone).to be_nil + end + + it_behaves_like 'has spam protection' do + let(:mutation_class) { ::Mutations::WorkItems::Convert } + end + end +end diff --git a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb index 97bf060356a..6a6ad1b14fd 100644 --- a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb @@ -23,7 +23,7 @@ RSpec.describe "Create a work item from a task in a work item's description", fe } end - let(:mutation) { graphql_mutation(:workItemCreateFromTask, input) } + let(:mutation) { graphql_mutation(:workItemCreateFromTask, input, nil, ['productAnalyticsState']) } let(:mutation_response) { graphql_mutation_response(:work_item_create_from_task) } context 'the user is not allowed to update a work item' do @@ -45,7 +45,6 @@ RSpec.describe "Create a work item from a task in a work item's description", fe expect(response).to have_gitlab_http_status(:success) expect(work_item.description).to eq("- [ ] #{created_work_item.to_reference}+") - expect(created_work_item.issue_type).to eq('task') expect(created_work_item.work_item_type.base_type).to eq('task') expect(created_work_item.work_item_parent).to eq(work_item) expect(created_work_item).to be_confidential diff --git a/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_spec.rb index 16f78b67b5c..fca3c84e534 100644 --- a/spec/requests/api/graphql/mutations/work_items/create_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb @@ -5,52 +5,43 @@ require 'spec_helper' RSpec.describe 'Create a work item', feature_category: :team_planning do include GraphqlHelpers - let_it_be(:project) { create(:project) } - let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:developer) { create(:user).tap { |user| group.add_developer(user) } } let(:input) do { 'title' => 'new title', 'description' => 'new description', 'confidential' => true, - 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_global_id.to_s + 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_gid.to_s } end - let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path)) } - + let(:fields) { nil } let(:mutation_response) { graphql_mutation_response(:work_item_create) } + let(:current_user) { developer } - context 'the user is not allowed to create a work item' do - let(:current_user) { create(:user) } - - it_behaves_like 'a mutation that returns a top-level access error' - end - - context 'when user has permissions to create a work item' do - let(:current_user) { developer } - + RSpec.shared_examples 'creates work item' do it 'creates the work item' do expect do post_graphql_mutation(mutation, current_user: current_user) end.to change(WorkItem, :count).by(1) created_work_item = WorkItem.last - expect(response).to have_gitlab_http_status(:success) - expect(created_work_item.issue_type).to eq('task') expect(created_work_item).to be_confidential expect(created_work_item.work_item_type.base_type).to eq('task') expect(mutation_response['workItem']).to include( input.except('workItemTypeId').merge( - 'id' => created_work_item.to_global_id.to_s, + 'id' => created_work_item.to_gid.to_s, 'workItemType' => hash_including('name' => 'Task') ) ) end context 'when input is invalid' do - let(:input) { { 'title' => '', 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_global_id.to_s } } + let(:input) { { 'title' => '', 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_gid.to_s } } it 'does not create and returns validation errors' do expect do @@ -90,16 +81,14 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do FIELDS end - let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) } - context 'when setting parent' do - let_it_be(:parent) { create(:work_item, project: project) } + let_it_be(:parent) { create(:work_item, **container_params) } let(:input) do { title: 'item1', - workItemTypeId: WorkItems::Type.default_by_type(:task).to_global_id.to_s, - hierarchyWidget: { 'parentId' => parent.to_global_id.to_s } + workItemTypeId: WorkItems::Type.default_by_type(:task).to_gid.to_s, + hierarchyWidget: { 'parentId' => parent.to_gid.to_s } } end @@ -110,14 +99,14 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do expect(widgets_response).to include( { 'children' => { 'edges' => [] }, - 'parent' => { 'id' => parent.to_global_id.to_s }, + 'parent' => { 'id' => parent.to_gid.to_s }, 'type' => 'HIERARCHY' } ) end context 'when parent work item type is invalid' do - let_it_be(:parent) { create(:work_item, :task, project: project) } + let_it_be(:parent) { create(:work_item, :task, **container_params) } it 'returns error' do post_graphql_mutation(mutation, current_user: current_user) @@ -137,6 +126,40 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do expect(graphql_errors.first['message']).to include('No object found for `parentId') end end + + context 'when adjacent is already in place' do + let_it_be(:adjacent) { create(:work_item, :task, **container_params) } + + let(:work_item) { WorkItem.last } + + let(:input) do + { + title: 'item1', + workItemTypeId: WorkItems::Type.default_by_type(:task).to_gid.to_s, + hierarchyWidget: { 'parentId' => parent.to_gid.to_s } + } + end + + before(:all) do + create(:parent_link, work_item_parent: parent, work_item: adjacent, relative_position: 0) + end + + it 'creates work item and sets the relative position to be AFTER adjacent' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change(WorkItem, :count).by(1) + + expect(response).to have_gitlab_http_status(:success) + expect(widgets_response).to include( + { + 'children' => { 'edges' => [] }, + 'parent' => { 'id' => parent.to_gid.to_s }, + 'type' => 'HIERARCHY' + } + ) + expect(work_item.parent_link.relative_position).to be > adjacent.parent_link.relative_position + end + end end context 'when unsupported widget input is sent' do @@ -144,7 +167,7 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do { 'title' => 'new title', 'description' => 'new description', - 'workItemTypeId' => WorkItems::Type.default_by_type(:test_case).to_global_id.to_s, + 'workItemTypeId' => WorkItems::Type.default_by_type(:test_case).to_gid.to_s, 'hierarchyWidget' => {} } end @@ -172,17 +195,15 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do FIELDS end - let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) } - context 'when setting milestone on work item creation' do let_it_be(:project_milestone) { create(:milestone, project: project) } - let_it_be(:group_milestone) { create(:milestone, project: project) } + let_it_be(:group_milestone) { create(:milestone, group: group) } let(:input) do { title: 'some WI', - workItemTypeId: WorkItems::Type.default_by_type(:task).to_global_id.to_s, - milestoneWidget: { 'milestoneId' => milestone.to_global_id.to_s } + workItemTypeId: WorkItems::Type.default_by_type(:task).to_gid.to_s, + milestoneWidget: { 'milestoneId' => milestone.to_gid.to_s } } end @@ -196,13 +217,18 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do expect(widgets_response).to include( { 'type' => 'MILESTONE', - 'milestone' => { 'id' => milestone.to_global_id.to_s } + 'milestone' => { 'id' => milestone.to_gid.to_s } } ) end end context 'when assigning a project milestone' do + before do + group_work_item = container_params[:namespace].present? + skip('cannot set a project level milestone to a group level work item') if group_work_item + end + it_behaves_like "work item's milestone is set" do let(:milestone) { project_milestone } end @@ -216,4 +242,66 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do end end end + + context 'the user is not allowed to create a work item' do + let(:current_user) { create(:user) } + let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) } + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user has permissions to create a work item' do + context 'when creating work items in a project' do + context 'with projectPath' do + let_it_be(:container_params) { { project: project } } + let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) } + + it_behaves_like 'creates work item' + end + + context 'with namespacePath' do + let_it_be(:container_params) { { project: project } } + let(:mutation) { graphql_mutation(:workItemCreate, input.merge('namespacePath' => project.full_path), fields) } + + it_behaves_like 'creates work item' + end + end + + context 'when creating work items in a group' do + let_it_be(:container_params) { { namespace: group } } + let(:mutation) { graphql_mutation(:workItemCreate, input.merge(namespacePath: group.full_path), fields) } + + it_behaves_like 'creates work item' + end + + context 'when both projectPath and namespacePath are passed' do + let_it_be(:container_params) { { project: project } } + let(:mutation) do + graphql_mutation( + :workItemCreate, + input.merge('projectPath' => project.full_path, 'namespacePath' => project.full_path), + fields + ) + end + + it_behaves_like 'a mutation that returns top-level errors', errors: [ + Mutations::WorkItems::Create::MUTUALLY_EXCLUSIVE_ARGUMENTS_ERROR + ] + end + + context 'when neither of projectPath nor namespacePath are passed' do + let_it_be(:container_params) { { project: project } } + let(:mutation) do + graphql_mutation( + :workItemCreate, + input, + fields + ) + end + + it_behaves_like 'a mutation that returns top-level errors', errors: [ + Mutations::WorkItems::Create::MUTUALLY_EXCLUSIVE_ARGUMENTS_ERROR + ] + end + end end diff --git a/spec/requests/api/graphql/mutations/work_items/export_spec.rb b/spec/requests/api/graphql/mutations/work_items/export_spec.rb new file mode 100644 index 00000000000..d5d07ea65f8 --- /dev/null +++ b/spec/requests/api/graphql/mutations/work_items/export_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Export work items', feature_category: :team_planning do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:reporter) { create(:user).tap { |user| project.add_reporter(user) } } + let_it_be(:guest) { create(:user).tap { |user| project.add_guest(user) } } + let_it_be(:work_item) { create(:work_item, project: project) } + + let(:input) { { 'projectPath' => project.full_path } } + let(:mutation) { graphql_mutation(:workItemExport, input) } + let(:mutation_response) { graphql_mutation_response(:work_item_export) } + + context 'when user is not allowed to export work items' do + let(:current_user) { guest } + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when import_export_work_items_csv feature flag is disabled' do + let(:current_user) { reporter } + + before do + stub_feature_flags(import_export_work_items_csv: false) + end + + it_behaves_like 'a mutation that returns top-level errors', + errors: ['`import_export_work_items_csv` feature flag is disabled.'] + end + + context 'when user has permissions to export work items' do + let(:current_user) { reporter } + let(:input) do + super().merge( + 'selectedFields' => %w[TITLE DESCRIPTION AUTHOR TYPE AUTHOR_USERNAME CREATED_AT], + 'authorUsername' => 'admin', + 'iids' => [work_item.iid.to_s], + 'state' => 'opened', + 'types' => 'TASK', + 'search' => 'any', + 'in' => 'TITLE' + ) + end + + it 'schedules export job with given arguments', :aggregate_failures do + expected_arguments = { + selected_fields: ['title', 'description', 'author', 'type', 'author username', 'created_at'], + author_username: 'admin', + iids: [work_item.iid.to_s], + state: 'opened', + issue_types: ['task'], + search: 'any', + in: ['title'] + } + + expect(IssuableExportCsvWorker) + .to receive(:perform_async).with(:work_item, current_user.id, project.id, expected_arguments) + + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['message']).to eq( + 'Your CSV export request has succeeded. The result will be emailed to ' \ + "#{reporter.notification_email_or_default}." + ) + expect(mutation_response['errors']).to be_empty + end + end +end diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb index ddd294e8f82..ce1c2c01faa 100644 --- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb @@ -7,20 +7,21 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, group: group) } + let_it_be(:author) { create(:user).tap { |user| project.add_reporter(user) } } let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } } let_it_be(:reporter) { create(:user).tap { |user| project.add_reporter(user) } } let_it_be(:guest) { create(:user).tap { |user| project.add_guest(user) } } - let_it_be(:work_item, refind: true) { create(:work_item, project: project) } + let_it_be(:work_item, refind: true) { create(:work_item, project: project, author: author) } let(:work_item_event) { 'CLOSE' } let(:input) { { 'stateEvent' => work_item_event, 'title' => 'updated title' } } let(:fields) do <<~FIELDS - workItem { - state - title - } - errors + workItem { + state + title + } + errors FIELDS end @@ -81,10 +82,10 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do context 'when updating confidentiality' do let(:fields) do <<~FIELDS - workItem { - confidential - } - errors + workItem { + confidential + } + errors FIELDS end @@ -126,18 +127,18 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do context 'with description widget input' do let(:fields) do <<~FIELDS - workItem { - title - description - state - widgets { - type - ... on WorkItemWidgetDescription { - description + workItem { + title + description + state + widgets { + type + ... on WorkItemWidgetDescription { + description + } } } - } - errors + errors FIELDS end @@ -445,31 +446,84 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do let(:widgets_response) { mutation_response['workItem']['widgets'] } let(:fields) do <<~FIELDS - workItem { - description - widgets { - type - ... on WorkItemWidgetHierarchy { - parent { - id - } - children { - edges { - node { - id + workItem { + description + widgets { + type + ... on WorkItemWidgetHierarchy { + parent { + id + } + children { + edges { + node { + id + } } } } } } - } - errors + errors FIELDS end + let_it_be(:valid_parent) { create(:work_item, project: project) } + let_it_be(:valid_child1) { create(:work_item, :task, project: project, created_at: 5.minutes.ago) } + let_it_be(:valid_child2) { create(:work_item, :task, project: project, created_at: 5.minutes.from_now) } + let(:input_base) { { parentId: valid_parent.to_gid.to_s } } + let(:child1_ref) { { adjacentWorkItemId: valid_child1.to_global_id.to_s } } + let(:child2_ref) { { adjacentWorkItemId: valid_child2.to_global_id.to_s } } + let(:relative_range) { [valid_child1, valid_child2].map(&:parent_link).map(&:relative_position) } + + let(:invalid_relative_position_error) do + WorkItems::Widgets::HierarchyService::UpdateService::INVALID_RELATIVE_POSITION_ERROR + end + + shared_examples 'updates work item parent and sets the relative position' do + it do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :work_item_parent).from(nil).to(valid_parent) + + expect(response).to have_gitlab_http_status(:success) + expect(widgets_response).to include({ 'type' => 'HIERARCHY', 'children' => { 'edges' => [] }, + 'parent' => { 'id' => valid_parent.to_global_id.to_s } }) + + expect(work_item.parent_link.relative_position).to be_between(*relative_range) + end + end + + shared_examples 'sets the relative position and does not update work item parent' do + it do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to not_change(work_item, :work_item_parent) + + expect(response).to have_gitlab_http_status(:success) + expect(widgets_response).to include({ 'type' => 'HIERARCHY', 'children' => { 'edges' => [] }, + 'parent' => { 'id' => valid_parent.to_global_id.to_s } }) + + expect(work_item.parent_link.relative_position).to be_between(*relative_range) + end + end + + shared_examples 'returns "relative position is not valid" error message' do + it do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to not_change(work_item, :work_item_parent) + + expect(mutation_response['workItem']).to be_nil + expect(mutation_response['errors']).to match_array([invalid_relative_position_error]) + end + end + context 'when updating parent' do let_it_be(:work_item, reload: true) { create(:work_item, :task, project: project) } - let_it_be(:valid_parent) { create(:work_item, project: project) } let_it_be(:invalid_parent) { create(:work_item, :task, project: project) } context 'when parent work item type is invalid' do @@ -492,20 +546,15 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do context 'when parent work item has a valid type' do let(:input) { { 'hierarchyWidget' => { 'parentId' => valid_parent.to_global_id.to_s } } } - it 'sets the parent for the work item' do + it 'updates work item parent' do expect do post_graphql_mutation(mutation, current_user: current_user) work_item.reload end.to change(work_item, :work_item_parent).from(nil).to(valid_parent) expect(response).to have_gitlab_http_status(:success) - expect(widgets_response).to include( - { - 'children' => { 'edges' => [] }, - 'parent' => { 'id' => valid_parent.to_global_id.to_s }, - 'type' => 'HIERARCHY' - } - ) + expect(widgets_response).to include({ 'type' => 'HIERARCHY', 'children' => { 'edges' => [] }, + 'parent' => { 'id' => valid_parent.to_global_id.to_s } }) end context 'when a parent is already present' do @@ -522,6 +571,31 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do end.to change(work_item, :work_item_parent).from(existing_parent).to(valid_parent) end end + + context 'when updating relative position' do + before(:all) do + create(:parent_link, work_item_parent: valid_parent, work_item: valid_child1) + create(:parent_link, work_item_parent: valid_parent, work_item: valid_child2) + end + + context "when incomplete positioning arguments are given" do + let(:input) { { hierarchyWidget: input_base.merge(child1_ref) } } + + it_behaves_like 'returns "relative position is not valid" error message' + end + + context 'when moving after adjacent' do + let(:input) { { hierarchyWidget: input_base.merge(child1_ref).merge(relativePosition: 'AFTER') } } + + it_behaves_like 'updates work item parent and sets the relative position' + end + + context 'when moving before adjacent' do + let(:input) { { hierarchyWidget: input_base.merge(child2_ref).merge(relativePosition: 'BEFORE') } } + + it_behaves_like 'updates work item parent and sets the relative position' + end + end end context 'when parentId is null' do @@ -577,9 +651,37 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do end end + context 'when reordering existing child' do + let_it_be(:work_item, reload: true) { create(:work_item, :task, project: project) } + + context "when parent is already assigned" do + before(:all) do + create(:parent_link, work_item_parent: valid_parent, work_item: work_item) + create(:parent_link, work_item_parent: valid_parent, work_item: valid_child1) + create(:parent_link, work_item_parent: valid_parent, work_item: valid_child2) + end + + context "when incomplete positioning arguments are given" do + let(:input) { { hierarchyWidget: child1_ref } } + + it_behaves_like 'returns "relative position is not valid" error message' + end + + context 'when moving after adjacent' do + let(:input) { { hierarchyWidget: child1_ref.merge(relativePosition: 'AFTER') } } + + it_behaves_like 'sets the relative position and does not update work item parent' + end + + context 'when moving before adjacent' do + let(:input) { { hierarchyWidget: child2_ref.merge(relativePosition: 'BEFORE') } } + + it_behaves_like 'sets the relative position and does not update work item parent' + end + end + end + context 'when updating children' do - let_it_be(:valid_child1) { create(:work_item, :task, project: project) } - let_it_be(:valid_child2) { create(:work_item, :task, project: project) } let_it_be(:invalid_child) { create(:work_item, project: project) } let(:input) { { 'hierarchyWidget' => { 'childrenIds' => children_ids } } } @@ -639,23 +741,29 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do context 'when updating assignees' do let(:fields) do <<~FIELDS - workItem { - widgets { - type - ... on WorkItemWidgetAssignees { - assignees { - nodes { - id - username + workItem { + title + workItemType { name } + widgets { + type + ... on WorkItemWidgetAssignees { + assignees { + nodes { + id + username + } } } - } - ... on WorkItemWidgetDescription { - description + ... on WorkItemWidgetDescription { + description + } + ... on WorkItemWidgetStartAndDueDate { + startDate + dueDate + } } } - } - errors + errors FIELDS end @@ -728,6 +836,79 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do ) end end + + context 'when changing work item type' do + let_it_be(:work_item) { create(:work_item, :task, project: project) } + let(:description) { "/type Issue" } + + let(:input) { { 'descriptionWidget' => { 'description' => description } } } + + context 'with multiple commands' do + let_it_be(:work_item) { create(:work_item, :task, project: project) } + + let(:description) { "Updating work item\n/type Issue\n/due tomorrow\n/title Foo" } + + it 'updates the work item type and other attributes' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change { work_item.work_item_type.base_type }.from('task').to('issue') + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']['workItemType']['name']).to eq('Issue') + expect(mutation_response['workItem']['title']).to eq('Foo') + expect(mutation_response['workItem']['widgets']).to include( + 'type' => 'START_AND_DUE_DATE', + 'dueDate' => Date.tomorrow.strftime('%Y-%m-%d'), + 'startDate' => nil + ) + end + end + + context 'when conversion is not permitted' do + let_it_be(:issue) { create(:work_item, project: project) } + let_it_be(:link) { create(:parent_link, work_item_parent: issue, work_item: work_item) } + + let(:error_msg) { 'Work item type cannot be changed to Issue with Issue as parent type.' } + + it 'does not update the work item type' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.not_to change { work_item.work_item_type.base_type } + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to include(error_msg) + end + end + + context 'when new type does not support a widget' do + before do + work_item.update!(start_date: Date.current, due_date: Date.tomorrow) + WorkItems::Type.default_by_type(:issue).widget_definitions + .find_by_widget_type(:start_and_due_date).update!(disabled: true) + end + + it 'updates the work item type and clear widget attributes' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change { work_item.work_item_type.base_type }.from('task').to('issue') + .and change { work_item.start_date }.to(nil) + .and change { work_item.start_date }.to(nil) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']['workItemType']['name']).to eq('Issue') + expect(mutation_response['workItem']['widgets']).to include( + { + 'type' => 'START_AND_DUE_DATE', + 'startDate' => nil, + 'dueDate' => nil + } + ) + end + end + end end context 'when the work item type does not support the assignees widget' do @@ -766,17 +947,17 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do let(:fields) do <<~FIELDS - workItem { - widgets { - type - ... on WorkItemWidgetMilestone { - milestone { - id + workItem { + widgets { + type + ... on WorkItemWidgetMilestone { + milestone { + id + } } } } - } - errors + errors FIELDS end @@ -843,18 +1024,427 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do end end + context 'when updating notifications subscription' do + let_it_be(:current_user) { reporter } + let(:input) { { 'notificationsWidget' => { 'subscribed' => desired_state } } } + + let(:fields) do + <<~FIELDS + workItem { + widgets { + type + ... on WorkItemWidgetNotifications { + subscribed + } + } + } + errors + FIELDS + end + + subject(:update_work_item) { post_graphql_mutation(mutation, current_user: current_user) } + + shared_examples 'subscription updated successfully' do + let_it_be(:subscription) do + create( + :subscription, project: project, + user: current_user, + subscribable: work_item, + subscribed: !desired_state + ) + end + + it "updates existing work item's subscription state" do + expect do + update_work_item + subscription.reload + end.to change(subscription, :subscribed).to(desired_state) + .and(change { work_item.reload.subscribed?(reporter, project) }.to(desired_state)) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']['widgets']).to include( + { + 'subscribed' => desired_state, + 'type' => 'NOTIFICATIONS' + } + ) + end + end + + shared_examples 'subscription update ignored' do + context 'when user is subscribed with a subscription record' do + let_it_be(:subscription) do + create( + :subscription, project: project, + user: current_user, + subscribable: work_item, + subscribed: !desired_state + ) + end + + it 'ignores the update request' do + expect do + update_work_item + subscription.reload + end.to not_change(subscription, :subscribed) + .and(not_change { work_item.subscribed?(current_user, project) }) + + expect(response).to have_gitlab_http_status(:success) + end + end + + context 'when user is subscribed by being a participant' do + let_it_be(:current_user) { author } + + it 'ignores the update request' do + expect do + update_work_item + end.to not_change(Subscription, :count) + .and(not_change { work_item.subscribed?(current_user, project) }) + + expect(response).to have_gitlab_http_status(:success) + end + end + end + + context 'when work item update fails' do + let_it_be(:desired_state) { false } + let(:input) { { 'title' => nil, 'notificationsWidget' => { 'subscribed' => desired_state } } } + + it_behaves_like 'subscription update ignored' + end + + context 'when user cannot update work item' do + let_it_be(:desired_state) { false } + + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(current_user, :update_subscription, work_item).and_return(false) + end + + it_behaves_like 'subscription update ignored' + end + + context 'when user can update work item' do + context 'when subscribing to notifications' do + let_it_be(:desired_state) { true } + + it_behaves_like 'subscription updated successfully' + end + + context 'when unsubscribing from notifications' do + let_it_be(:desired_state) { false } + + it_behaves_like 'subscription updated successfully' + + context 'when user is subscribed by being a participant' do + let_it_be(:current_user) { author } + + it 'creates a subscription with desired state' do + expect { update_work_item }.to change(Subscription, :count).by(1) + .and(change { work_item.reload.subscribed?(author, project) }.to(desired_state)) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']['widgets']).to include( + { + 'subscribed' => desired_state, + 'type' => 'NOTIFICATIONS' + } + ) + end + end + end + end + end + + context 'when updating currentUserTodos' do + let_it_be(:current_user) { reporter } + + let(:fields) do + <<~FIELDS + workItem { + widgets { + type + ... on WorkItemWidgetCurrentUserTodos { + currentUserTodos { + nodes { + id + state + } + } + } + } + } + errors + FIELDS + end + + subject(:update_work_item) { post_graphql_mutation(mutation, current_user: current_user) } + + context 'when adding a new todo' do + let(:input) { { 'currentUserTodosWidget' => { 'action' => 'ADD' } } } + + context 'when user has access to the work item' do + it 'adds a new todo for the user on the work item' do + expect { update_work_item }.to change { current_user.todos.count }.by(1) + + created_todo = current_user.todos.last + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']['widgets']).to include( + { + 'type' => 'CURRENT_USER_TODOS', + 'currentUserTodos' => { + 'nodes' => [ + { 'id' => created_todo.to_global_id.to_s, 'state' => 'pending' } + ] + } + } + ) + end + end + + context 'when user has no access' do + let_it_be(:current_user) { create(:user) } + + it 'does not create a new todo' do + expect { update_work_item }.to change { Todo.count }.by(0) + + expect(response).to have_gitlab_http_status(:success) + end + end + end + + context 'when marking all todos of the work item as done' do + let_it_be(:pending_todo1) do + create(:todo, target: work_item, target_type: 'WorkItem', user: current_user, state: :pending) + end + + let_it_be(:pending_todo2) do + create(:todo, target: work_item, target_type: 'WorkItem', user: current_user, state: :pending) + end + + let(:input) { { 'currentUserTodosWidget' => { 'action' => 'MARK_AS_DONE' } } } + + context 'when user has access' do + it 'marks all todos of the user on the work item as done' do + expect { update_work_item }.to change { current_user.todos.done.count }.by(2) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']['widgets']).to include( + { + 'type' => 'CURRENT_USER_TODOS', + 'currentUserTodos' => { + 'nodes' => match_array([ + { 'id' => pending_todo1.to_global_id.to_s, 'state' => 'done' }, + { 'id' => pending_todo2.to_global_id.to_s, 'state' => 'done' } + ]) + } + } + ) + end + end + + context 'when user has no access' do + let_it_be(:current_user) { create(:user) } + + it 'does not mark todos as done' do + expect { update_work_item }.to change { Todo.done.count }.by(0) + + expect(response).to have_gitlab_http_status(:success) + end + end + end + + context 'when marking one todo of the work item as done' do + let_it_be(:pending_todo1) do + create(:todo, target: work_item, target_type: 'WorkItem', user: current_user, state: :pending) + end + + let_it_be(:pending_todo2) do + create(:todo, target: work_item, target_type: 'WorkItem', user: current_user, state: :pending) + end + + let(:input) do + { 'currentUserTodosWidget' => { 'action' => 'MARK_AS_DONE', todo_id: global_id_of(pending_todo1) } } + end + + context 'when user has access' do + it 'marks the todo of the work item as done' do + expect { update_work_item }.to change { current_user.todos.done.count }.by(1) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']['widgets']).to include( + { + 'type' => 'CURRENT_USER_TODOS', + 'currentUserTodos' => { + 'nodes' => match_array([ + { 'id' => pending_todo1.to_global_id.to_s, 'state' => 'done' }, + { 'id' => pending_todo2.to_global_id.to_s, 'state' => 'pending' } + ]) + } + } + ) + end + end + + context 'when user has no access' do + let_it_be(:current_user) { create(:user) } + + it 'does not mark the todo as done' do + expect { update_work_item }.to change { Todo.done.count }.by(0) + + expect(response).to have_gitlab_http_status(:success) + end + end + end + end + + context 'when updating awardEmoji' do + let_it_be(:current_user) { work_item.author } + let_it_be(:upvote) { create(:award_emoji, :upvote, awardable: work_item, user: current_user) } + let(:award_action) { 'ADD' } + let(:award_name) { 'star' } + let(:input) { { 'awardEmojiWidget' => { 'action' => award_action, 'name' => award_name } } } + + let(:fields) do + <<~FIELDS + workItem { + widgets { + type + ... on WorkItemWidgetAwardEmoji { + upvotes + downvotes + awardEmoji { + nodes { + name + user { id } + } + } + } + } + } + errors + FIELDS + end + + subject(:update_work_item) { post_graphql_mutation(mutation, current_user: current_user) } + + context 'when user cannot award work item' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(current_user, :award_emoji, work_item).and_return(false) + end + + it 'ignores the update request' do + expect do + update_work_item + end.to not_change(AwardEmoji, :count) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to be_empty + expect(graphql_errors).to be_blank + end + end + + context 'when user can award work item' do + shared_examples 'request with error' do |message| + it 'ignores update and returns an error' do + expect do + update_work_item + end.not_to change(AwardEmoji, :count) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']).to be_nil + expect(mutation_response['errors'].first).to include(message) + end + end + + shared_examples 'request that removes emoji' do + it "updates work item's award emoji" do + expect do + update_work_item + end.to change(AwardEmoji, :count).by(-1) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']['widgets']).to include( + { + 'upvotes' => 0, + 'downvotes' => 0, + 'awardEmoji' => { 'nodes' => [] }, + 'type' => 'AWARD_EMOJI' + } + ) + end + end + + shared_examples 'request that adds emoji' do + it "updates work item's award emoji" do + expect do + update_work_item + end.to change(AwardEmoji, :count).by(1) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']['widgets']).to include( + { + 'upvotes' => 1, + 'downvotes' => 0, + 'awardEmoji' => { 'nodes' => [ + { 'name' => 'thumbsup', 'user' => { 'id' => current_user.to_gid.to_s } }, + { 'name' => award_name, 'user' => { 'id' => current_user.to_gid.to_s } } + ] }, + 'type' => 'AWARD_EMOJI' + } + ) + end + end + + context 'when adding award emoji' do + it_behaves_like 'request that adds emoji' + + context 'when the emoji name is not valid' do + let(:award_name) { 'xxqq' } + + it_behaves_like 'request with error', 'Name is not a valid emoji name' + end + end + + context 'when removing award emoji' do + let(:award_action) { 'REMOVE' } + + context 'when emoji was awarded by current user' do + let(:award_name) { 'thumbsup' } + + it_behaves_like 'request that removes emoji' + end + + context 'when emoji was awarded by a different user' do + let(:award_name) { 'thumbsdown' } + + before do + create(:award_emoji, :downvote, awardable: work_item) + end + + it_behaves_like 'request with error', + 'User has not awarded emoji of type thumbsdown on the awardable' + end + end + end + end + context 'when unsupported widget input is sent' do - let_it_be(:test_case) { create(:work_item_type, :default, :test_case) } - let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) } + let_it_be(:work_item) { create(:work_item, :test_case, project: project) } let(:input) do { - 'hierarchyWidget' => {} + 'assigneesWidget' => { 'assigneeIds' => [developer.to_gid.to_s] } } end it_behaves_like 'a mutation that returns top-level errors', - errors: ["Following widget keys are not supported by Test Case type: [:hierarchy_widget]"] + errors: ["Following widget keys are not supported by Test Case type: [:assignees_widget]"] end end end diff --git a/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb index 999c685ac6a..717de983871 100644 --- a/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb @@ -18,7 +18,7 @@ RSpec.describe 'Update a work item task', feature_category: :team_planning do let(:task_params) { { 'title' => 'UPDATED' } } let(:task_input) { { 'id' => task.to_global_id.to_s }.merge(task_params) } let(:input) { { 'id' => work_item.to_global_id.to_s, 'taskData' => task_input } } - let(:mutation) { graphql_mutation(:workItemUpdateTask, input) } + let(:mutation) { graphql_mutation(:workItemUpdateTask, input, nil, ['productAnalyticsState']) } let(:mutation_response) { graphql_mutation_response(:work_item_update_task) } context 'the user is not allowed to read a work item' do diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb index 4e12da3e3ab..83edacaf831 100644 --- a/spec/requests/api/graphql/namespace/projects_spec.rb +++ b/spec/requests/api/graphql/namespace/projects_spec.rb @@ -23,7 +23,7 @@ RSpec.describe 'getting projects', feature_category: :projects do projects(includeSubgroups: #{include_subgroups}) { edges { node { - #{all_graphql_fields_for('Project', max_depth: 1)} + #{all_graphql_fields_for('Project', max_depth: 1, excluded: ['productAnalyticsState'])} } } } diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb index 82fcc5254ad..7610a4aaac1 100644 --- a/spec/requests/api/graphql/packages/package_spec.rb +++ b/spec/requests/api/graphql/packages/package_spec.rb @@ -270,6 +270,31 @@ RSpec.describe 'package details', feature_category: :package_registry do it 'returns composer_config_repository_url correctly' do expect(graphql_data_at(:package, :composer_config_repository_url)).to eq("localhost/#{group.id}") end + + context 'with access to package registry for everyone' do + before do + project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC) + subject + end + + it 'returns pypi_url correctly' do + expect(graphql_data_at(:package, :pypi_url)).to eq("http://__token__:<your_personal_token>@localhost/api/v4/projects/#{project.id}/packages/pypi/simple") + end + end + + context 'when project is public' do + let_it_be(:public_project) { create(:project, :public, group: group) } + let_it_be(:composer_package) { create(:composer_package, project: public_project) } + let(:package_global_id) { global_id_of(composer_package) } + + before do + subject + end + + it 'returns pypi_url correctly' do + expect(graphql_data_at(:package, :pypi_url)).to eq("http://localhost/api/v4/projects/#{public_project.id}/packages/pypi/simple") + end + end end context 'web_path' do diff --git a/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb index b430fdeb18f..3417f9529bd 100644 --- a/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :projects do +RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :incident_management do include GraphqlHelpers let_it_be(:project) { create(:project) } @@ -29,6 +29,7 @@ RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :pr let(:first_alert) { alerts.first } before do + stub_feature_flags(remove_monitor_metrics: false) project.add_developer(current_user) end @@ -44,6 +45,17 @@ RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :pr expect(first_alert).to include('metricsDashboardUrl' => dashboard_url_for_alert) end + + context 'when metrics dashboard feature is unavailable' do + before do + stub_feature_flags(remove_monitor_metrics: true) + end + + it 'returns nil' do + post_graphql(graphql_query, current_user: current_user) + expect(first_alert['metricsDashboardUrl']).to be_nil + end + end end context 'with gitlab-managed prometheus payload' do @@ -58,5 +70,16 @@ RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :pr expect(first_alert).to include('metricsDashboardUrl' => dashboard_url_for_alert) end + + context 'when metrics dashboard feature is unavailable' do + before do + stub_feature_flags(remove_monitor_metrics: true) + end + + it 'returns nil' do + post_graphql(graphql_query, current_user: current_user) + expect(first_alert['metricsDashboardUrl']).to be_nil + end + end end end diff --git a/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb index 16dd0dfcfcb..c1ac0367853 100644 --- a/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb @@ -51,7 +51,7 @@ RSpec.describe 'getting Alert Management Alert Notes', feature_category: :team_p expect(first_notes_result.first).to include( 'id' => first_system_note.to_global_id.to_s, - 'systemNoteIconName' => 'git-merge', + 'systemNoteIconName' => 'merge', 'body' => first_system_note.note ) end diff --git a/spec/requests/api/graphql/project/base_service_spec.rb b/spec/requests/api/graphql/project/base_service_spec.rb index 7b1b95eaf58..b27cddea07b 100644 --- a/spec/requests/api/graphql/project/base_service_spec.rb +++ b/spec/requests/api/graphql/project/base_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'query Jira service', feature_category: :authentication_and_authorization do +RSpec.describe 'query Jira service', feature_category: :system_access do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb b/spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb new file mode 100644 index 00000000000..dd76f6425fe --- /dev/null +++ b/spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Project.ci_access_authorized_agents', feature_category: :deployment_management do + include GraphqlHelpers + + let_it_be(:organization) { create(:group) } + let_it_be(:agent_management_project) { create(:project, :private, group: organization) } + let_it_be(:agent) { create(:cluster_agent, project: agent_management_project) } + + let_it_be(:deployment_project) { create(:project, :private, group: organization) } + let_it_be(:deployment_developer) { create(:user).tap { |u| deployment_project.add_developer(u) } } + let_it_be(:deployment_reporter) { create(:user).tap { |u| deployment_project.add_reporter(u) } } + + let(:user) { deployment_developer } + + let(:query) do + %( + query { + project(fullPath: "#{deployment_project.full_path}") { + ciAccessAuthorizedAgents { + nodes { + agent { + id + name + project { + name + } + } + config + } + } + } + } + ) + end + + subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } + + context 'with project authorization' do + let!(:ci_access) { create(:agent_ci_access_project_authorization, agent: agent, project: deployment_project) } + + it 'returns the authorized agent' do + authorized_agents = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes') + + expect(authorized_agents.count).to eq(1) + + authorized_agent = authorized_agents.first + + expect(authorized_agent['agent']['id']).to eq(agent.to_global_id.to_s) + expect(authorized_agent['agent']['name']).to eq(agent.name) + expect(authorized_agent['config']).to eq({ "default_namespace" => "production" }) + expect(authorized_agent['agent']['project']).to be_nil # User is not authorized to read other resources. + end + + context 'when user is developer in the agent management project' do + before do + agent_management_project.add_developer(deployment_developer) + end + + it 'returns the project information as well' do + authorized_agent = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes').first + + expect(authorized_agent['agent']['project']['name']).to eq(agent_management_project.name) + end + end + + context 'when user is reporter' do + let(:user) { deployment_reporter } + + it 'returns nothing' do + expect(subject['data']['project']['ciAccessAuthorizedAgents']).to be_nil + end + end + end + + context 'with group authorization' do + let!(:ci_access) { create(:agent_ci_access_group_authorization, agent: agent, group: organization) } + + it 'returns the authorized agent' do + authorized_agents = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes') + + expect(authorized_agents.count).to eq(1) + + authorized_agent = authorized_agents.first + + expect(authorized_agent['agent']['id']).to eq(agent.to_global_id.to_s) + expect(authorized_agent['agent']['name']).to eq(agent.name) + expect(authorized_agent['config']).to eq({ "default_namespace" => "production" }) + expect(authorized_agent['agent']['project']).to be_nil # User is not authorized to read other resources. + end + + context 'when user is developer in the agent management project' do + before do + agent_management_project.add_developer(deployment_developer) + end + + it 'returns the project information as well' do + authorized_agent = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes').first + + expect(authorized_agent['agent']['project']['name']).to eq(agent_management_project.name) + end + end + + context 'when user is reporter' do + let(:user) { deployment_reporter } + + it 'returns nothing' do + expect(subject['data']['project']['ciAccessAuthorizedAgents']).to be_nil + end + end + end + + context 'when deployment project is not authorized to ci_access to the agent' do + it 'returns empty' do + authorized_agents = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes') + + expect(authorized_agents).to be_empty + 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 index 0881eb9cdc3..181f21001ea 100644 --- a/spec/requests/api/graphql/project/cluster_agents_spec.rb +++ b/spec/requests/api/graphql/project/cluster_agents_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Project.cluster_agents', feature_category: :kubernetes_management do +RSpec.describe 'Project.cluster_agents', feature_category: :deployment_management do include GraphqlHelpers let_it_be(:project) { create(:project, :public) } @@ -53,10 +53,11 @@ RSpec.describe 'Project.cluster_agents', feature_category: :kubernetes_managemen 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_it_be(:revoked_token) { create(:cluster_agent_token, :revoked, agent: agents.second) } let(:cluster_agents_fields) { [:id, query_nodes(:tokens, of: 'ClusterAgentToken')] } - it 'can select tokens in last_used_at order' do + it 'can select active tokens in last_used_at order' do post_graphql(query, current_user: current_user) tokens = graphql_data_at(:project, :cluster_agents, :nodes, :tokens, :nodes) diff --git a/spec/requests/api/graphql/project/commit_references_spec.rb b/spec/requests/api/graphql/project/commit_references_spec.rb new file mode 100644 index 00000000000..4b545adee12 --- /dev/null +++ b/spec/requests/api/graphql/project/commit_references_spec.rb @@ -0,0 +1,240 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.project(fullPath).commitReferences(commitSha)', feature_category: :source_code_management do + include GraphqlHelpers + include Presentable + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:repository) { project.repository.raw } + let_it_be(:current_user) { project.first_owner } + let_it_be(:branches_names) { %w[master not-merged-branch v1.1.0] } + let_it_be(:tag_name) { 'v1.0.0' } + let_it_be(:commit_sha) { repository.commit.id } + + let(:post_query) { post_graphql(query, current_user: current_user) } + let(:data) { graphql_data.dig(*path) } + let(:base_args) { {} } + let(:args) { base_args } + + shared_context 'with the limit argument' do + context 'with limit of 2' do + let(:args) { { limit: 2 } } + + it 'returns the right amount of refs' do + post_query + expect(data.count).to be <= 2 + end + end + + context 'with limit of -2' do + let(:args) { { limit: -2 } } + + it 'casts an argument error "limit must be greater then 0"' do + post_query + expect(graphql_errors).to include(custom_graphql_error(path - ['names'], + 'limit must be within 1..1000')) + end + end + + context 'with limit of 1001' do + let(:args) { { limit: 1001 } } + + it 'casts an argument error "limit must be greater then 0"' do + post_query + expect(graphql_errors).to include(custom_graphql_error(path - ['names'], + 'limit must be within 1..1000')) + end + end + end + + describe 'the path commitReferences should return nil' do + let(:path) { %w[project commitReferences] } + + let(:query) do + graphql_query_for(:project, { fullPath: project.full_path }, + query_graphql_field( + :commitReferences, + { commitSha: commit_sha }, + query_graphql_field(:tippingTags, :names) + ) + ) + end + + context 'when commit does not exist' do + let(:commit_sha) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff4' } + + it 'commitReferences returns nil' do + post_query + expect(data).to eq(nil) + end + end + + context 'when sha length is incorrect' do + let(:commit_sha) { 'foo' } + + it 'commitReferences returns nil' do + post_query + expect(data).to eq(nil) + end + end + + context 'when user is not authorized' do + let(:commit_sha) { repository.commit.id } + let(:current_user) { create(:user) } + + it 'commitReferences returns nil' do + post_query + expect(data).to eq(nil) + end + end + end + + context 'with containing refs' do + let(:base_args) { { excludeTipped: false } } + let(:excluded_tipped_args) do + hash = base_args.dup + hash[:excludeTipped] = true + hash + end + + context 'with path Query.project(fullPath).commitReferences(commitSha).containingTags' do + let_it_be(:commit_sha) { repository.find_tag(tag_name).target_commit.sha } + let_it_be(:path) { %w[project commitReferences containingTags names] } + let(:query) do + graphql_query_for( + :project, + { fullPath: project.full_path }, + query_graphql_field( + :commitReferences, + { commitSha: commit_sha }, + query_graphql_field(:containingTags, args, :names) + ) + ) + end + + context 'without excludeTipped argument' do + it 'returns tags names containing the commit' do + post_query + expect(data).to eq(%w[v1.0.0 v1.1.0 v1.1.1]) + end + end + + context 'with excludeTipped argument' do + let_it_be(:ref_prefix) { Gitlab::Git::TAG_REF_PREFIX } + + let(:args) { excluded_tipped_args } + + it 'returns tags names containing the commit without the tipped tags' do + excluded_refs = project.repository + .refs_by_oid(oid: commit_sha, ref_patterns: [ref_prefix]) + .map { |n| n.delete_prefix(ref_prefix) } + + post_query + expect(data).to eq(%w[v1.0.0 v1.1.0 v1.1.1] - excluded_refs) + end + end + + include_context 'with the limit argument' + end + + context 'with path Query.project(fullPath).commitReferences(commitSha).containingBranches' do + let_it_be(:ref_prefix) { Gitlab::Git::BRANCH_REF_PREFIX } + let_it_be(:path) { %w[project commitReferences containingBranches names] } + + let(:query) do + graphql_query_for( + :project, + { fullPath: project.full_path }, + query_graphql_field( + :commitReferences, + { commitSha: commit_sha }, + query_graphql_field(:containingBranches, args, :names) + ) + ) + end + + context 'without excludeTipped argument' do + it 'returns branch names containing the commit' do + refs = project.repository.branch_names_contains(commit_sha) + + post_query + + expect(data).to eq(refs) + end + end + + context 'with excludeTipped argument' do + let(:args) { excluded_tipped_args } + + it 'returns branch names containing the commit without the tipped branch' do + refs = project.repository.branch_names_contains(commit_sha) + + excluded_refs = project.repository + .refs_by_oid(oid: commit_sha, ref_patterns: [ref_prefix]) + .map { |n| n.delete_prefix(ref_prefix) } + + post_query + + expect(data).to eq(refs - excluded_refs) + end + end + + include_context 'with the limit argument' + end + end + + context 'with tipping refs' do + context 'with path Query.project(fullPath).commitReferences(commitSha).tippingTags' do + let(:commit_sha) { repository.find_tag(tag_name).dereferenced_target.sha } + let(:path) { %w[project commitReferences tippingTags names] } + + let(:query) do + graphql_query_for( + :project, + { fullPath: project.full_path }, + query_graphql_field( + :commitReferences, + { commitSha: commit_sha }, + query_graphql_field(:tippingTags, args, :names) + ) + ) + end + + context 'with authorized user' do + it 'returns tags names tipping the commit' do + post_query + + expect(data).to eq([tag_name]) + end + end + + include_context 'with the limit argument' + end + + context 'with path Query.project(fullPath).commitReferences(commitSha).tippingBranches' do + let(:path) { %w[project commitReferences tippingBranches names] } + + let(:query) do + graphql_query_for( + :project, + { fullPath: project.full_path }, + query_graphql_field( + :commitReferences, + { commitSha: commit_sha }, + query_graphql_field(:tippingBranches, args, :names) + ) + ) + end + + it 'returns branches names tipping the commit' do + post_query + + expect(data).to eq(branches_names) + end + + include_context 'with the limit argument' + 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 7ccf8a6f5bf..9a40a972256 100644 --- a/spec/requests/api/graphql/project/container_repositories_spec.rb +++ b/spec/requests/api/graphql/project/container_repositories_spec.rb @@ -12,7 +12,7 @@ RSpec.describe 'getting container repositories in a project', feature_category: 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(:excluded_fields) { %w[pipeline jobs productAnalyticsState] } let(:container_repositories_fields) do <<~GQL edges { @@ -155,7 +155,7 @@ RSpec.describe 'getting container repositories in a project', feature_category: 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] } + let(:excluded_fields) { %w[pipeline jobs tags tagsCount productAnalyticsState] } end it 'returns the total count of container repositories' do diff --git a/spec/requests/api/graphql/project/data_transfer_spec.rb b/spec/requests/api/graphql/project/data_transfer_spec.rb new file mode 100644 index 00000000000..aafa8d65eb9 --- /dev/null +++ b/spec/requests/api/graphql/project/data_transfer_spec.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'project data transfers', feature_category: :source_code_management do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project) } + + let(:fields) do + <<~QUERY + #{all_graphql_fields_for('ProjectDataTransfer'.classify)} + QUERY + end + + let(:query) do + graphql_query_for( + 'project', + { fullPath: project.full_path }, + query_graphql_field('DataTransfer', params, fields) + ) + end + + let(:from) { Date.new(2022, 1, 1) } + let(:to) { Date.new(2023, 1, 1) } + let(:params) { { from: from, to: to } } + let(:egress_data) do + graphql_data.dig('project', 'dataTransfer', 'egressNodes', 'nodes') + end + + before do + create(:project_data_transfer, project: project, date: '2022-01-01', repository_egress: 1) + create(:project_data_transfer, project: project, date: '2022-02-01', repository_egress: 2) + end + + subject { post_graphql(query, current_user: current_user) } + + context 'with anonymous access' do + let_it_be(:current_user) { nil } + + before do + subject + end + + it_behaves_like 'a working graphql query' + + it 'returns no data' do + expect(graphql_data_at(:project, :data_transfer)).to be_nil + expect(graphql_errors).to be_nil + end + end + + context 'with authorized user but without enough permissions' do + before do + project.add_developer(current_user) + subject + end + + it_behaves_like 'a working graphql query' + + it 'returns empty results' do + expect(graphql_data_at(:project, :data_transfer)).to be_nil + expect(graphql_errors).to be_nil + end + end + + context 'when user has enough permissions' do + before do + project.add_owner(current_user) + end + + context 'when data_transfer_monitoring_mock_data is NOT enabled' do + before do + stub_feature_flags(data_transfer_monitoring_mock_data: false) + subject + end + + it 'returns real results' do + expect(response).to have_gitlab_http_status(:ok) + + expect(egress_data.count).to eq(2) + + expect(egress_data.first.keys).to match_array( + %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress] + ) + + expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 2]) + end + + it_behaves_like 'a working graphql query' + end + + context 'when data_transfer_monitoring_mock_data is enabled' do + before do + stub_feature_flags(data_transfer_monitoring_mock_data: true) + subject + end + + it 'returns mock results' do + expect(response).to have_gitlab_http_status(:ok) + + expect(egress_data.count).to eq(12) + expect(egress_data.first.keys).to match_array( + %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress] + ) + end + + it_behaves_like 'a working graphql query' + end + end +end diff --git a/spec/requests/api/graphql/project/environments_spec.rb b/spec/requests/api/graphql/project/environments_spec.rb index 618f591affa..bb1763ee228 100644 --- a/spec/requests/api/graphql/project/environments_spec.rb +++ b/spec/requests/api/graphql/project/environments_spec.rb @@ -102,7 +102,7 @@ RSpec.describe 'Project Environments query', feature_category: :continuous_deliv end describe 'last deployments of environments' do - ::Deployment.statuses.each do |status, _| + ::Deployment.statuses.each do |status, _| # rubocop:disable RSpec/UselessDynamicDefinition let_it_be(:"production_#{status}_deployment") do create(:deployment, status.to_sym, environment: production, project: project) end diff --git a/spec/requests/api/graphql/project/flow_metrics_spec.rb b/spec/requests/api/graphql/project/flow_metrics_spec.rb new file mode 100644 index 00000000000..3b5758b3a2e --- /dev/null +++ b/spec/requests/api/graphql/project/flow_metrics_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting project flow metrics', feature_category: :value_stream_management do + include GraphqlHelpers + + let_it_be(:group) { create(:group) } + let_it_be(:project1) { create(:project, :repository, group: group) } + # This is done so we can use the same count expectations in the shared examples and + # reuse the shared example for the group-level test. + let_it_be(:project2) { project1 } + let_it_be(:production_environment1) { create(:environment, :production, project: project1) } + let_it_be(:production_environment2) { production_environment1 } + let_it_be(:current_user) { create(:user, maintainer_projects: [project1]) } + + let(:full_path) { project1.full_path } + let(:context) { :project } + + it_behaves_like 'value stream analytics flow metrics issueCount examples' + + it_behaves_like 'value stream analytics flow metrics deploymentCount examples' +end diff --git a/spec/requests/api/graphql/project/fork_details_spec.rb b/spec/requests/api/graphql/project/fork_details_spec.rb index efd48b00833..91a04dc7c50 100644 --- a/spec/requests/api/graphql/project/fork_details_spec.rb +++ b/spec/requests/api/graphql/project/fork_details_spec.rb @@ -10,12 +10,13 @@ RSpec.describe 'getting project fork details', feature_category: :source_code_ma let_it_be(:current_user) { create(:user, maintainer_projects: [project]) } let_it_be(:forked_project) { fork_project(project, current_user, repository: true) } + let(:ref) { 'feature' } let(:queried_project) { forked_project } let(:query) do graphql_query_for(:project, { full_path: queried_project.full_path }, <<~QUERY - forkDetails(ref: "feature"){ + forkDetails(ref: "#{ref}"){ ahead behind } @@ -23,12 +24,23 @@ RSpec.describe 'getting project fork details', feature_category: :source_code_ma ) end - it 'returns fork details' do - post_graphql(query, current_user: current_user) + context 'when a ref is specified' do + using RSpec::Parameterized::TableSyntax - expect(graphql_data['project']['forkDetails']).to eq( - { 'ahead' => 1, 'behind' => 29 } - ) + where(:ref, :counts) do + 'feature' | { 'ahead' => 1, 'behind' => 29 } + 'v1.1.1' | { 'ahead' => 5, 'behind' => 0 } + '7b5160f9bb23a3d58a0accdbe89da13b96b1ece9' | { 'ahead' => 9, 'behind' => 0 } + 'non-existent-branch' | { 'ahead' => nil, 'behind' => nil } + end + + with_them do + it 'returns fork details' do + post_graphql(query, current_user: current_user) + + expect(graphql_data['project']['forkDetails']).to eq(counts) + end + end end context 'when a project is not a fork' do @@ -41,6 +53,16 @@ RSpec.describe 'getting project fork details', feature_category: :source_code_ma end end + context 'when project source is not visible' do + it 'does not return fork details' do + project.team.truncate + + post_graphql(query, current_user: current_user) + + expect(graphql_data['project']['forkDetails']).to be_nil + end + end + context 'when a user cannot read the code' do let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb index 76e5d687fd1..80c7258c05d 100644 --- a/spec/requests/api/graphql/project/merge_request_spec.rb +++ b/spec/requests/api/graphql/project/merge_request_spec.rb @@ -480,4 +480,31 @@ RSpec.describe 'getting merge request information nested in a project', feature_ merge_request.assignees << user end end + + context 'when selecting `awardEmoji`' do + let_it_be(:award_emoji) { create(:award_emoji, awardable: merge_request, user: current_user) } + + let(:mr_fields) do + <<~QUERY + awardEmoji { + nodes { + user { + username + } + name + } + } + QUERY + end + + it 'includes award emojis' do + post_graphql(query, current_user: current_user) + + response = merge_request_graphql_data['awardEmoji']['nodes'] + + expect(response.length).to eq(1) + expect(response.first['user']['username']).to eq(current_user.username) + expect(response.first['name']).to eq(award_emoji.name) + end + end end diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb index 8407faa967e..e3c4396e7d8 100644 --- a/spec/requests/api/graphql/project/merge_requests_spec.rb +++ b/spec/requests/api/graphql/project/merge_requests_spec.rb @@ -226,6 +226,28 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat it_behaves_like 'when searching with parameters' end + context 'when searching by approved' do + let(:approved_mr) { create(:merge_request, target_project: project, source_project: project) } + + before do + create(:approval, merge_request: approved_mr) + end + + context 'when true' do + let(:search_params) { { approved: true } } + let(:mrs) { [approved_mr] } + + it_behaves_like 'when searching with parameters' + end + + context 'when false' do + let(:search_params) { { approved: false } } + let(:mrs) { all_merge_requests } + + it_behaves_like 'when searching with parameters' + end + end + context 'when requesting `approved_by`' do let(:search_params) { { iids: [merge_request_a.iid.to_s, merge_request_b.iid.to_s] } } let(:extra_iid_for_second_query) { merge_request_c.iid.to_s } @@ -331,7 +353,7 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat end context 'when award emoji votes' do - let(:requested_fields) { [:upvotes, :downvotes] } + let(:requested_fields) { 'upvotes downvotes awardEmoji { nodes { name } }' } before do create_list(:award_emoji, 2, name: 'thumbsup', awardable: merge_request_a) @@ -588,8 +610,9 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat end let(:query) do + # Adding a no-op `not` filter to mimic the same query as the frontend does graphql_query_for(:project, { full_path: project.full_path }, <<~QUERY) - mergeRequests(mergedAfter: "2020-01-01", mergedBefore: "2020-01-05", first: 0) { + mergeRequests(mergedAfter: "2020-01-01", mergedBefore: "2020-01-05", first: 0, not: { labels: null }) { totalTimeToMerge count } diff --git a/spec/requests/api/graphql/project/milestones_spec.rb b/spec/requests/api/graphql/project/milestones_spec.rb index 3b31da77a75..7a79bf2184a 100644 --- a/spec/requests/api/graphql/project/milestones_spec.rb +++ b/spec/requests/api/graphql/project/milestones_spec.rb @@ -137,18 +137,6 @@ RSpec.describe 'getting milestone listings nested in a project', feature_categor it_behaves_like 'searching with parameters' end - context 'searching by custom range' do - let(:expected) { [no_end, fully_future] } - let(:search_params) do - { - start_date: (today + 6.days).iso8601, - end_date: (today + 7.days).iso8601 - } - end - - it_behaves_like 'searching with parameters' - end - context 'using timeframe argument' do let(:expected) { [no_end, fully_future] } let(:search_params) do @@ -188,23 +176,6 @@ RSpec.describe 'getting milestone listings nested in a project', feature_categor end end - it 'is invalid to provide timeframe and start_date/end_date' do - query = <<~GQL - query($path: ID!, $tstart: Date!, $tend: Date!, $start: Time!, $end: Time!) { - project(fullPath: $path) { - milestones(timeframe: { start: $tstart, end: $tend }, startDate: $start, endDate: $end) { - nodes { id } - } - } - } - GQL - - post_graphql(query, current_user: current_user, - variables: vars.merge(vars.transform_keys { |k| :"t#{k}" })) - - expect(graphql_errors).to contain_exactly(a_hash_including('message' => include('deprecated in favor of timeframe'))) - end - it 'is invalid to invert the timeframe arguments' do query = <<~GQL query($path: ID!, $start: Date!, $end: Date!) { diff --git a/spec/requests/api/graphql/project/project_statistics_redirect_spec.rb b/spec/requests/api/graphql/project/project_statistics_redirect_spec.rb new file mode 100644 index 00000000000..8049a75ace3 --- /dev/null +++ b/spec/requests/api/graphql/project/project_statistics_redirect_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'rendering project storage type routes', feature_category: :shared do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :public) } + let_it_be(:user) { create(:user) } + + let(:query) do + graphql_query_for('project', + { 'fullPath' => project.full_path }, + "statisticsDetailsPaths { #{all_graphql_fields_for('ProjectStatisticsRedirect')} }") + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: user) + end + end + + shared_examples 'valid routes for storage type' do + it 'contains all keys' do + post_graphql(query, current_user: user) + + expect(graphql_data['project']['statisticsDetailsPaths'].keys).to match_array( + %w[repository buildArtifacts wiki packages snippets containerRegistry] + ) + end + + it 'contains valid paths' do + repository_url = Gitlab::Routing.url_helpers.project_tree_url(project, "master") + wiki_url = Gitlab::Routing.url_helpers.project_wikis_pages_url(project) + build_artifacts_url = Gitlab::Routing.url_helpers.project_artifacts_url(project) + packages_url = Gitlab::Routing.url_helpers.project_packages_url(project) + snippets_url = Gitlab::Routing.url_helpers.project_snippets_url(project) + container_registry_url = Gitlab::Routing.url_helpers.project_container_registry_index_url(project) + + post_graphql(query, current_user: user) + + expect(graphql_data['project']['statisticsDetailsPaths'].values).to match_array [repository_url, + wiki_url, + build_artifacts_url, + packages_url, + snippets_url, + container_registry_url] + end + end + + context 'when project is public' do + it_behaves_like 'valid routes for storage type' + + context 'when user is nil' do + let_it_be(:user) { nil } + + it_behaves_like 'valid routes for storage type' + end + end + + context 'when project is private' do + let_it_be(:project) { create(:project, :private) } + + before do + project.add_reporter(user) + end + + it_behaves_like 'valid routes for storage type' + + context 'when user is nil' do + it 'hides statisticsDetailsPaths for nil users' do + post_graphql(query, current_user: nil) + + expect(graphql_data['project']).to be_blank + end + end + end +end diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb index 477388585ca..8d4a39d6b30 100644 --- a/spec/requests/api/graphql/project/release_spec.rb +++ b/spec/requests/api/graphql/project/release_spec.rb @@ -132,7 +132,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re let(:release_fields) do query_graphql_field(:assets, nil, - query_graphql_field(:links, nil, 'nodes { id name url external, directAssetUrl }')) + query_graphql_field(:links, nil, 'nodes { id name url, directAssetUrl }')) end it 'finds all release links' do @@ -141,7 +141,6 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re expected = release.links.map do |link| a_graphql_entity_for( link, :name, :url, - 'external' => link.external?, 'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url ) end @@ -322,16 +321,15 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re let(:release_fields) do query_graphql_field(:assets, nil, - query_graphql_field(:links, nil, 'nodes { id name url external, directAssetUrl }')) + query_graphql_field(:links, nil, 'nodes { id name url, directAssetUrl }')) end - it 'finds all non source external release links' do + it 'finds all non source release links' do post_query expected = release.links.map do |link| a_graphql_entity_for( link, :name, :url, - 'external' => true, 'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url ) end diff --git a/spec/requests/api/graphql/project/user_access_authorized_agents_spec.rb b/spec/requests/api/graphql/project/user_access_authorized_agents_spec.rb new file mode 100644 index 00000000000..b8017171fd1 --- /dev/null +++ b/spec/requests/api/graphql/project/user_access_authorized_agents_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Project.user_access_authorized_agents', feature_category: :deployment_management do + include GraphqlHelpers + + let_it_be(:organization) { create(:group) } + let_it_be(:agent_management_project) { create(:project, :private, group: organization) } + let_it_be(:agent) { create(:cluster_agent, project: agent_management_project) } + + let_it_be(:deployment_project) { create(:project, :private, group: organization) } + let_it_be(:deployment_developer) { create(:user).tap { |u| deployment_project.add_developer(u) } } + let_it_be(:deployment_reporter) { create(:user).tap { |u| deployment_project.add_reporter(u) } } + + let(:user) { deployment_developer } + + let(:query) do + %( + query { + project(fullPath: "#{deployment_project.full_path}") { + userAccessAuthorizedAgents { + nodes { + agent { + id + name + project { + name + } + } + config + } + } + } + } + ) + end + + subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } + + context 'with project authorization' do + let!(:user_access) { create(:agent_user_access_project_authorization, agent: agent, project: deployment_project) } + + it 'returns the authorized agent' do + authorized_agents = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes') + + expect(authorized_agents.count).to eq(1) + + authorized_agent = authorized_agents.first + + expect(authorized_agent['agent']['id']).to eq(agent.to_global_id.to_s) + expect(authorized_agent['agent']['name']).to eq(agent.name) + expect(authorized_agent['config']).to eq({}) + expect(authorized_agent['agent']['project']).to be_nil # User is not authorized to read other resources. + end + + context 'when user is developer in the agent management project' do + before do + agent_management_project.add_developer(deployment_developer) + end + + it 'returns the project information as well' do + authorized_agent = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes').first + + expect(authorized_agent['agent']['project']['name']).to eq(agent_management_project.name) + end + end + + context 'when user is reporter' do + let(:user) { deployment_reporter } + + it 'returns nothing' do + expect(subject['data']['project']['userAccessAuthorizedAgents']).to be_nil + end + end + end + + context 'with group authorization' do + let_it_be(:deployment_group) { create(:group, :private, parent: organization) } + + let!(:user_access) { create(:agent_user_access_group_authorization, agent: agent, group: deployment_group) } + + before_all do + deployment_group.add_developer(deployment_developer) + deployment_group.add_reporter(deployment_reporter) + end + + it 'returns the authorized agent' do + authorized_agents = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes') + + expect(authorized_agents.count).to eq(1) + + authorized_agent = authorized_agents.first + + expect(authorized_agent['agent']['id']).to eq(agent.to_global_id.to_s) + expect(authorized_agent['agent']['name']).to eq(agent.name) + expect(authorized_agent['config']).to eq({}) + expect(authorized_agent['agent']['project']).to be_nil # User is not authorized to read other resources. + end + + context 'when user is developer in the agent management project' do + before do + agent_management_project.add_developer(deployment_developer) + end + + it 'returns the project information as well' do + authorized_agent = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes').first + + expect(authorized_agent['agent']['project']['name']).to eq(agent_management_project.name) + end + end + + context 'when user is reporter' do + let(:user) { deployment_reporter } + + it 'returns nothing' do + expect(subject['data']['project']['userAccessAuthorizedAgents']).to be_nil + end + end + end + + context 'when deployment project is not authorized to user_access to the agent' do + it 'returns empty' do + authorized_agents = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes') + + expect(authorized_agents).to be_empty + end + end +end diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb index f49165a88ea..628a2117e9d 100644 --- a/spec/requests/api/graphql/project/work_items_spec.rb +++ b/spec/requests/api/graphql/project/work_items_spec.rb @@ -120,24 +120,55 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team end context 'when querying WorkItemWidgetHierarchy' do - let_it_be(:children) { create_list(:work_item, 3, :task, project: project) } + let_it_be(:children) { create_list(:work_item, 4, :task, project: project) } let_it_be(:child_link1) { create(:parent_link, work_item_parent: item1, work_item: children[0]) } + let_it_be(:child_link2) { create(:parent_link, work_item_parent: item1, work_item: children[1]) } let(:fields) do <<~GRAPHQL - nodes { - widgets { - type - ... on WorkItemWidgetHierarchy { - hasChildren - parent { id } - children { nodes { id } } - } + nodes { + id + widgets { + type + ... on WorkItemWidgetHierarchy { + hasChildren + parent { id } + children { nodes { id } } } } + } GRAPHQL end + context 'with ordered children' do + let(:items_data) { graphql_data['project']['workItems']['nodes'] } + let(:work_item_data) { items_data.find { |item| item['id'] == item1.to_gid.to_s } } + let(:work_item_widget) { work_item_data["widgets"].find { |widget| widget.key?("children") } } + let(:children_ids) { work_item_widget.dig("children", "nodes").pluck("id") } + + let(:first_child) { children[0].to_gid.to_s } + let(:second_child) { children[1].to_gid.to_s } + + it 'returns children ordered by created_at by default' do + post_graphql(query, current_user: current_user) + + expect(children_ids).to eq([first_child, second_child]) + end + + context 'when ordered by relative position' do + before do + child_link1.update!(relative_position: 20) + child_link2.update!(relative_position: 10) + end + + it 'returns children in correct order' do + post_graphql(query, current_user: current_user) + + expect(children_ids).to eq([second_child, first_child]) + end + end + end + it 'executes limited number of N+1 queries' do post_graphql(query, current_user: current_user) # warm-up @@ -146,13 +177,11 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team end parent_work_items = create_list(:work_item, 2, project: project) - create(:parent_link, work_item_parent: parent_work_items[0], work_item: children[1]) - create(:parent_link, work_item_parent: parent_work_items[1], work_item: children[2]) + create(:parent_link, work_item_parent: parent_work_items[0], work_item: children[2]) + create(:parent_link, work_item_parent: parent_work_items[1], work_item: children[3]) - # There are 2 extra queries for fetching the children field - # See: https://gitlab.com/gitlab-org/gitlab/-/issues/363569 expect { post_graphql(query, current_user: current_user) } - .not_to exceed_query_limit(control).with_threshold(2) + .not_to exceed_query_limit(control) end it 'avoids N+1 queries when children are added to a work item' do @@ -162,8 +191,8 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team post_graphql(query, current_user: current_user) end - create(:parent_link, work_item_parent: item1, work_item: children[1]) create(:parent_link, work_item_parent: item1, work_item: children[2]) + create(:parent_link, work_item_parent: item1, work_item: children[3]) expect { post_graphql(query, current_user: current_user) } .not_to exceed_query_limit(control) @@ -313,6 +342,79 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team end end + context 'when fetching work item notifications widget' do + let(:fields) do + <<~GRAPHQL + nodes { + widgets { + type + ... on WorkItemWidgetNotifications { + subscribed + } + } + } + GRAPHQL + end + + it 'executes limited number of N+1 queries', :use_sql_query_cache do + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: current_user) + end + + create_list(:work_item, 3, project: project) + + # Performs 1 extra query per item to fetch subscriptions + expect { post_graphql(query, current_user: current_user) } + .not_to exceed_all_query_limit(control).with_threshold(3) + expect_graphql_errors_to_be_empty + end + end + + context 'when fetching work item award emoji widget' do + let(:fields) do + <<~GRAPHQL + nodes { + widgets { + type + ... on WorkItemWidgetAwardEmoji { + awardEmoji { + nodes { + name + emoji + user { id } + } + } + upvotes + downvotes + } + } + } + GRAPHQL + end + + before do + create(:award_emoji, name: 'star', user: current_user, awardable: item1) + create(:award_emoji, :upvote, awardable: item1) + create(:award_emoji, :downvote, awardable: item1) + end + + it 'executes limited number of N+1 queries', :use_sql_query_cache do + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: current_user) + end + + create_list(:work_item, 2, project: project) do |item| + create(:award_emoji, name: 'rocket', awardable: item) + create_list(:award_emoji, 2, :upvote, awardable: item) + create_list(:award_emoji, 2, :downvote, awardable: item) + end + + expect { post_graphql(query, current_user: current_user) } + .not_to exceed_all_query_limit(control) + expect_graphql_errors_to_be_empty + end + end + def item_ids graphql_dig_at(items_data, :node, :id) end diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb index 281a08e6548..9f51258c163 100644 --- a/spec/requests/api/graphql/project_query_spec.rb +++ b/spec/requests/api/graphql/project_query_spec.rb @@ -120,6 +120,67 @@ RSpec.describe 'getting project information', feature_category: :projects do end end + describe 'is_catalog_resource' do + before do + project.add_owner(current_user) + end + + let(:catalog_resource_query) do + <<~GRAPHQL + { + project(fullPath: "#{project.full_path}") { + isCatalogResource + } + } + GRAPHQL + end + + context 'when the project is not a catalog resource' do + it 'is false' do + post_graphql(catalog_resource_query, current_user: current_user) + + expect(graphql_data.dig('project', 'isCatalogResource')).to be(false) + end + end + + context 'when the project is a catalog resource' do + before do + create(:catalog_resource, project: project) + end + + it 'is true' do + post_graphql(catalog_resource_query, current_user: current_user) + + expect(graphql_data.dig('project', 'isCatalogResource')).to be(true) + end + end + + context 'for N+1 queries with isCatalogResource' do + let_it_be(:project1) { create(:project, group: group) } + let_it_be(:project2) { create(:project, group: group) } + + it 'avoids N+1 database queries' do + pending('See: https://gitlab.com/gitlab-org/gitlab/-/issues/403634') + ctx = { current_user: current_user } + + baseline_query = graphql_query_for(:project, { full_path: project1.full_path }, 'isCatalogResource') + + query = <<~GQL + query { + a: #{query_graphql_field(:project, { full_path: project1.full_path }, 'isCatalogResource')} + b: #{query_graphql_field(:project, { full_path: project2.full_path }, 'isCatalogResource')} + } + GQL + + control = ActiveRecord::QueryRecorder.new do + run_with_clean_state(baseline_query, context: ctx) + end + + expect { run_with_clean_state(query, context: ctx) }.not_to exceed_query_limit(control) + end + end + end + context 'when the user has reporter access to the project' do let(:statistics_query) do <<~GRAPHQL diff --git a/spec/requests/api/graphql/query_spec.rb b/spec/requests/api/graphql/query_spec.rb index 2b9d66ec744..0602cfec149 100644 --- a/spec/requests/api/graphql/query_spec.rb +++ b/spec/requests/api/graphql/query_spec.rb @@ -2,10 +2,10 @@ require 'spec_helper' -RSpec.describe 'Query', feature_category: :not_owned do +RSpec.describe 'Query', feature_category: :shared do include GraphqlHelpers - let_it_be(:project) { create(:project) } + let_it_be(:project) { create(:project, public_builds: false) } let_it_be(:issue) { create(:issue, project: project) } let_it_be(:developer) { create(:user) } @@ -116,4 +116,36 @@ RSpec.describe 'Query', feature_category: :not_owned do end end end + + describe '.ciPipelineStage' do + let_it_be(:ci_stage) { create(:ci_stage, name: 'graphql test stage', project: project) } + + let(:query) do + <<~GRAPHQL + { + ciPipelineStage(id: "#{ci_stage.to_global_id}") { + name + } + } + GRAPHQL + end + + context 'when the current user has access to the stage' do + it 'fetches the stage for the given ID' do + project.add_developer(developer) + + post_graphql(query, current_user: developer) + + expect(graphql_data.dig('ciPipelineStage', 'name')).to eq('graphql test stage') + end + end + + context 'when the current user does not have access to the stage' do + it 'returns nil' do + post_graphql(query, current_user: developer) + + expect(graphql_data['ciPipelineStage']).to be_nil + end + end + end end diff --git a/spec/requests/api/graphql/user/user_achievements_query_spec.rb b/spec/requests/api/graphql/user/user_achievements_query_spec.rb new file mode 100644 index 00000000000..27d32d07372 --- /dev/null +++ b/spec/requests/api/graphql/user/user_achievements_query_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'UserAchievements', feature_category: :user_profile do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group, :private) } + let_it_be(:achievement) { create(:achievement, namespace: group) } + let_it_be(:non_revoked_achievement) { create(:user_achievement, achievement: achievement, user: user) } + let_it_be(:revoked_achievement) { create(:user_achievement, :revoked, achievement: achievement, user: user) } + let_it_be(:fields) do + <<~HEREDOC + userAchievements { + nodes { + id + achievement { + id + } + user { + id + } + awardedByUser { + id + } + revokedByUser { + id + } + } + } + HEREDOC + end + + let_it_be(:query) do + graphql_query_for('user', { id: user.to_global_id.to_s }, fields) + end + + let(:current_user) { user } + + before_all do + group.add_guest(user) + end + + before do + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + + it 'returns all non_revoked user_achievements' do + expect(graphql_data_at(:user, :userAchievements, :nodes)).to contain_exactly( + a_graphql_entity_for(non_revoked_achievement) + ) + end + + it 'can lookahead to eliminate N+1 queries', :use_clean_rails_memory_store_caching do + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: user) + end.count + + achievement2 = create(:achievement, namespace: group) + create_list(:user_achievement, 2, achievement: achievement2, user: user) + + expect { post_graphql(query, current_user: user) }.not_to exceed_all_query_limit(control_count) + end + + context 'when the achievements feature flag is disabled for a namespace' do + let_it_be(:group2) { create(:group) } + let_it_be(:achievement2) { create(:achievement, namespace: group2) } + let_it_be(:user_achievement2) { create(:user_achievement, achievement: achievement2, user: user) } + + before do + stub_feature_flags(achievements: false) + stub_feature_flags(achievements: group2) + post_graphql(query, current_user: current_user) + end + + it 'does not return user_achievements for that namespace' do + expect(graphql_data_at(:user, :userAchievements, :nodes)).to contain_exactly( + a_graphql_entity_for(user_achievement2) + ) + end + end + + context 'when current user is not a member of the private group' do + let(:current_user) { create(:user) } + + it 'returns all achievements' do + expect(graphql_data_at(:user, :userAchievements, :nodes)).to contain_exactly( + a_graphql_entity_for(non_revoked_achievement) + ) + end + end +end diff --git a/spec/requests/api/graphql/user_spec.rb b/spec/requests/api/graphql/user_spec.rb index c19dfa6f3f3..41ee233dfc5 100644 --- a/spec/requests/api/graphql/user_spec.rb +++ b/spec/requests/api/graphql/user_spec.rb @@ -10,6 +10,12 @@ RSpec.describe 'User', feature_category: :user_profile do shared_examples 'a working user query' do it_behaves_like 'a working graphql query' do before do + # TODO: This license stub is necessary because the remote development workspaces field + # defined in the EE version of UserInterface gets picked up here and thus the license + # check happens. This comes from the `ancestors` call in + # lib/graphql/schema/member/has_fields.rb#fields in the graphql library. + stub_licensed_features(remote_development: true) + post_graphql(query, current_user: current_user) end end @@ -36,9 +42,17 @@ RSpec.describe 'User', feature_category: :user_profile do end context 'when username parameter is used' do - let(:query) { graphql_query_for(:user, { username: current_user.username.to_s }) } + context 'when username is identically cased' do + let(:query) { graphql_query_for(:user, { username: current_user.username.to_s }) } - it_behaves_like 'a working user query' + it_behaves_like 'a working user query' + end + + context 'when username is differently cased' do + let(:query) { graphql_query_for(:user, { username: current_user.username.to_s.upcase }) } + + it_behaves_like 'a working user query' + end end context 'when username and id parameter are used' do diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index 0fad4f4ff3a..dc5004a121b 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -36,9 +36,15 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do end context 'when the user can read the work item' do + let(:incoming_email_token) { current_user.incoming_email_token } + let(:work_item_email) do + "p+#{project.full_path_slug}-#{project.project_id}-#{incoming_email_token}-issue-#{work_item.iid}@gl.ab" + end + before do project.add_developer(developer) project.add_guest(guest) + stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") post_graphql(query, current_user: current_user) end @@ -55,11 +61,15 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do 'title' => work_item.title, 'confidential' => work_item.confidential, 'workItemType' => hash_including('id' => work_item.work_item_type.to_gid.to_s), + 'reference' => work_item.to_reference, + 'createNoteEmail' => work_item_email, 'userPermissions' => { 'readWorkItem' => true, 'updateWorkItem' => true, 'deleteWorkItem' => false, - 'adminWorkItem' => true + 'adminWorkItem' => true, + 'adminParentLink' => true, + 'setWorkItemMetadata' => true }, 'project' => hash_including('id' => project.to_gid.to_s, 'fullPath' => project.full_path) ) @@ -373,6 +383,161 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do ) end end + + describe 'notifications widget' do + let(:work_item_fields) do + <<~GRAPHQL + id + widgets { + type + ... on WorkItemWidgetNotifications { + subscribed + } + } + GRAPHQL + end + + it 'returns widget information' do + expect(work_item_data).to include( + 'id' => work_item.to_gid.to_s, + 'widgets' => include( + hash_including( + 'type' => 'NOTIFICATIONS', + 'subscribed' => work_item.subscribed?(current_user, project) + ) + ) + ) + end + end + + describe 'currentUserTodos widget' do + let_it_be(:current_user) { developer } + let_it_be(:other_todo) { create(:todo, state: :pending, user: current_user) } + + let_it_be(:done_todo) do + create(:todo, state: :done, target: work_item, target_type: work_item.class.name, user: current_user) + end + + let_it_be(:pending_todo) do + create(:todo, state: :pending, target: work_item, target_type: work_item.class.name, user: current_user) + end + + let_it_be(:other_user_todo) do + create(:todo, state: :pending, target: work_item, target_type: work_item.class.name, user: create(:user)) + end + + let(:work_item_fields) do + <<~GRAPHQL + id + widgets { + type + ... on WorkItemWidgetCurrentUserTodos { + currentUserTodos { + nodes { + id + state + } + } + } + } + GRAPHQL + end + + context 'with access' do + it 'returns widget information' do + expect(work_item_data).to include( + 'id' => work_item.to_gid.to_s, + 'widgets' => include( + hash_including( + 'type' => 'CURRENT_USER_TODOS', + 'currentUserTodos' => { + 'nodes' => match_array( + [done_todo, pending_todo].map { |t| { 'id' => t.to_gid.to_s, 'state' => t.state } } + ) + } + ) + ) + ) + end + end + + context 'with filter' do + let(:work_item_fields) do + <<~GRAPHQL + id + widgets { + type + ... on WorkItemWidgetCurrentUserTodos { + currentUserTodos(state: done) { + nodes { + id + state + } + } + } + } + GRAPHQL + end + + it 'returns widget information' do + expect(work_item_data).to include( + 'id' => work_item.to_gid.to_s, + 'widgets' => include( + hash_including( + 'type' => 'CURRENT_USER_TODOS', + 'currentUserTodos' => { + 'nodes' => match_array( + [done_todo].map { |t| { 'id' => t.to_gid.to_s, 'state' => t.state } } + ) + } + ) + ) + ) + end + end + end + + describe 'award emoji widget' do + let_it_be(:emoji) { create(:award_emoji, name: 'star', awardable: work_item) } + let_it_be(:upvote) { create(:award_emoji, :upvote, awardable: work_item) } + let_it_be(:downvote) { create(:award_emoji, :downvote, awardable: work_item) } + + let(:work_item_fields) do + <<~GRAPHQL + id + widgets { + type + ... on WorkItemWidgetAwardEmoji { + upvotes + downvotes + awardEmoji { + nodes { + name + } + } + } + } + GRAPHQL + end + + it 'returns widget information' do + expect(work_item_data).to include( + 'id' => work_item.to_gid.to_s, + 'widgets' => include( + hash_including( + 'type' => 'AWARD_EMOJI', + 'upvotes' => work_item.upvotes, + 'downvotes' => work_item.downvotes, + 'awardEmoji' => { + 'nodes' => match_array( + [emoji, upvote, downvote].map { |e| { 'name' => e.name } } + ) + } + ) + ) + ) + end + end end context 'when an Issue Global ID is provided' do @@ -398,4 +563,23 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do ) end end + + context 'when the user cannot set work item metadata' do + let(:current_user) { guest } + + before do + project.add_guest(guest) + post_graphql(query, current_user: current_user) + end + + it 'returns correct user permission' do + expect(work_item_data).to include( + 'id' => work_item.to_gid.to_s, + 'userPermissions' => + hash_including( + 'setWorkItemMetadata' => false + ) + ) + end + end end diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb index d7724371cce..8a3c5261eb6 100644 --- a/spec/requests/api/graphql_spec.rb +++ b/spec/requests/api/graphql_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe 'GraphQL', feature_category: :not_owned do +RSpec.describe 'GraphQL', feature_category: :shared do include GraphqlHelpers include AfterNextHelpers diff --git a/spec/requests/api/group_clusters_spec.rb b/spec/requests/api/group_clusters_spec.rb index 68c3af01e56..58d0e6a1eb5 100644 --- a/spec/requests/api/group_clusters_spec.rb +++ b/spec/requests/api/group_clusters_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::GroupClusters, feature_category: :kubernetes_management do +RSpec.describe API::GroupClusters, feature_category: :deployment_management do include KubernetesHelpers let(:current_user) { create(:user) } diff --git a/spec/requests/api/group_milestones_spec.rb b/spec/requests/api/group_milestones_spec.rb index 91f64d02d43..2f05b0fcf21 100644 --- a/spec/requests/api/group_milestones_spec.rb +++ b/spec/requests/api/group_milestones_spec.rb @@ -4,35 +4,41 @@ require 'spec_helper' RSpec.describe API::GroupMilestones, feature_category: :team_planning do let_it_be(:user) { create(:user) } - let_it_be(:group) { create(:group, :private) } + let_it_be_with_refind(:group) { create(:group, :private) } let_it_be(:project) { create(:project, namespace: group) } let_it_be(:group_member) { create(:group_member, group: group, user: user) } - let_it_be(:closed_milestone) { create(:closed_milestone, group: group, title: 'version1', description: 'closed milestone') } - let_it_be(:milestone) { create(:milestone, group: group, title: 'version2', description: 'open milestone') } + let_it_be(:closed_milestone) do + create(:closed_milestone, group: group, title: 'version1', description: 'closed milestone') + end + + let_it_be_with_reload(:milestone) do + create(:milestone, group: group, title: 'version2', description: 'open milestone', updated_at: 4.days.ago) + end let(:route) { "/groups/#{group.id}/milestones" } + shared_examples 'listing all milestones' do + it 'returns correct list of milestones' do + get api(route, user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(milestones.size) + expect(json_response.map { |entry| entry["id"] }).to eq(milestones.map(&:id)) + end + end + it_behaves_like 'group and project milestones', "/groups/:id/milestones" describe 'GET /groups/:id/milestones' do - context 'when include_parent_milestones is true' do - let_it_be(:ancestor_group) { create(:group, :private) } - let_it_be(:ancestor_group_milestone) { create(:milestone, group: ancestor_group) } - let_it_be(:params) { { include_parent_milestones: true } } - - before_all do - group.update!(parent: ancestor_group) - end + let_it_be(:ancestor_group) { create(:group, :private) } + let_it_be(:ancestor_group_milestone) { create(:milestone, group: ancestor_group, updated_at: 2.days.ago) } - shared_examples 'listing all milestones' do - it 'returns correct list of milestones' do - get api(route, user), params: params + before_all do + group.update!(parent: ancestor_group) + end - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.size).to eq(milestones.size) - expect(json_response.map { |entry| entry["id"] }).to eq(milestones.map(&:id)) - end - end + context 'when include_parent_milestones is true' do + let(:params) { { include_parent_milestones: true } } context 'when user has access to ancestor groups' do let(:milestones) { [ancestor_group_milestone, milestone, closed_milestone] } @@ -45,10 +51,26 @@ RSpec.describe API::GroupMilestones, feature_category: :team_planning do it_behaves_like 'listing all milestones' context 'when iids param is present' do - let_it_be(:params) { { include_parent_milestones: true, iids: [milestone.iid] } } + let(:params) { { include_parent_milestones: true, iids: [milestone.iid] } } it_behaves_like 'listing all milestones' end + + context 'when updated_before param is present' do + let(:params) { { updated_before: 1.day.ago.iso8601, include_parent_milestones: true } } + + it_behaves_like 'listing all milestones' do + let(:milestones) { [ancestor_group_milestone, milestone] } + end + end + + context 'when updated_after param is present' do + let(:params) { { updated_after: 1.day.ago.iso8601, include_parent_milestones: true } } + + it_behaves_like 'listing all milestones' do + let(:milestones) { [closed_milestone] } + end + end end context 'when user has no access to ancestor groups' do @@ -63,6 +85,22 @@ RSpec.describe API::GroupMilestones, feature_category: :team_planning do end end end + + context 'when updated_before param is present' do + let(:params) { { updated_before: 1.day.ago.iso8601 } } + + it_behaves_like 'listing all milestones' do + let(:milestones) { [milestone] } + end + end + + context 'when updated_after param is present' do + let(:params) { { updated_after: 1.day.ago.iso8601 } } + + it_behaves_like 'listing all milestones' do + let(:milestones) { [closed_milestone] } + end + end end describe 'GET /groups/:id/milestones/:milestone_id/issues' do diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb index e3d538d72ba..6849b087211 100644 --- a/spec/requests/api/group_variables_spec.rb +++ b/spec/requests/api/group_variables_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::GroupVariables, feature_category: :pipeline_authoring do +RSpec.describe API::GroupVariables, feature_category: :secrets_management do let_it_be(:group) { create(:group) } let_it_be(:user) { create(:user) } let_it_be(:variable) { create(:ci_group_variable, group: group) } diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 12a6553f51a..84d48b4edb4 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -6,6 +6,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do include GroupAPIHelpers include UploadHelpers include WorkhorseHelpers + include KeysetPaginationHelpers let_it_be(:user1) { create(:user, can_create_group: false) } let_it_be(:user2) { create(:user) } @@ -39,7 +40,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do context 'when invalid' do shared_examples 'invalid file upload request' do - it 'returns 400' do + it 'returns 400', :aggregate_failures do make_upload_request expect(response).to have_gitlab_http_status(:bad_request) @@ -65,7 +66,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end shared_examples 'skips searching in full path' do - it 'does not find groups by full path' do + it 'does not find groups by full path', :aggregate_failures do subgroup = create(:group, parent: parent, path: "#{parent.path}-subgroup") create(:group, parent: parent, path: 'not_matching_path') @@ -79,7 +80,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do describe "GET /groups" do context "when unauthenticated" do - it "returns public groups" do + it "returns public groups", :aggregate_failures do get api("/groups") expect(response).to have_gitlab_http_status(:ok) @@ -93,18 +94,18 @@ RSpec.describe API::Groups, feature_category: :subgroups do it 'avoids N+1 queries', :use_sql_query_cache do control = ActiveRecord::QueryRecorder.new(skip_cached: false) do - get api("/groups", admin) + get api("/groups") end create(:group) expect do - get api("/groups", admin) + get api("/groups") end.not_to exceed_all_query_limit(control) end context 'when statistics are requested' do - it 'does not include statistics' do + it 'does not include statistics', :aggregate_failures do get api("/groups"), params: { statistics: true } expect(response).to have_gitlab_http_status(:ok) @@ -116,7 +117,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context "when authenticated as user" do - it "normal user: returns an array of groups of user1" do + it "normal user: returns an array of groups of user1", :aggregate_failures do get api("/groups", user1) expect(response).to have_gitlab_http_status(:ok) @@ -127,7 +128,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do .to satisfy_one { |group| group['name'] == group1.name } end - it "does not include runners_token information" do + it "does not include runners_token information", :aggregate_failures do get api("/groups", user1) expect(response).to have_gitlab_http_status(:ok) @@ -137,7 +138,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.first).not_to include('runners_token') end - it "does not include statistics" do + it "does not include statistics", :aggregate_failures do get api("/groups", user1), params: { statistics: true } expect(response).to have_gitlab_http_status(:ok) @@ -146,7 +147,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.first).not_to include 'statistics' end - it "includes a created_at timestamp" do + it "includes a created_at timestamp", :aggregate_failures do get api("/groups", user1) expect(response).to have_gitlab_http_status(:ok) @@ -175,7 +176,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'on making requests below the allowed offset pagination threshold' do - it 'paginates the records' do + it 'paginates the records', :aggregate_failures do get api('/groups'), params: { page: 1, per_page: 1 } expect(response).to have_gitlab_http_status(:ok) @@ -196,25 +197,8 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'keyset pagination' do - def pagination_links(response) - link = response.headers['LINK'] - return unless link - - link.split(',').map do |link| - match = link.match(/<(?<url>.*)>; rel="(?<rel>\w+)"/) - break nil unless match - - { url: match[:url], rel: match[:rel] } - end.compact - end - - def params_for_next_page(response) - next_url = pagination_links(response).find { |link| link[:rel] == 'next' }[:url] - Rack::Utils.parse_query(URI.parse(next_url).query) - end - context 'on making requests with supported ordering structure' do - it 'paginates the records correctly' do + it 'paginates the records correctly', :aggregate_failures do # first page get api('/groups'), params: { pagination: 'keyset', per_page: 1 } @@ -223,7 +207,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(records.size).to eq(1) expect(records.first['id']).to eq(group_1.id) - params_for_next_page = params_for_next_page(response) + params_for_next_page = pagination_params_from_next_url(response) expect(params_for_next_page).to include('cursor') get api('/groups'), params: params_for_next_page @@ -236,7 +220,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'on making requests with unsupported ordering structure' do - it 'returns error' do + it 'returns error', :aggregate_failures do get api('/groups'), params: { pagination: 'keyset', per_page: 1, order_by: 'path', sort: 'desc' } expect(response).to have_gitlab_http_status(:method_not_allowed) @@ -248,8 +232,8 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context "when authenticated as admin" do - it "admin: returns an array of all groups" do - get api("/groups", admin) + it "admin: returns an array of all groups", :aggregate_failures do + get api("/groups", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -257,8 +241,8 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.length).to eq(2) end - it "does not include runners_token information" do - get api("/groups", admin) + it "does not include runners_token information", :aggregate_failures do + get api("/groups", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -267,8 +251,8 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.first).not_to include('runners_token') end - it "does not include statistics by default" do - get api("/groups", admin) + it "does not include statistics by default", :aggregate_failures do + get api("/groups", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -276,8 +260,8 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.first).not_to include('statistics') end - it "includes a created_at timestamp" do - get api("/groups", admin) + it "includes a created_at timestamp", :aggregate_failures do + get api("/groups", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -285,7 +269,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.first['created_at']).to be_present end - it "includes statistics if requested" do + it "includes statistics if requested", :aggregate_failures do attributes = { storage_size: 4093, repository_size: 123, @@ -302,7 +286,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do project1.statistics.update!(attributes) - get api("/groups", admin), params: { statistics: true } + get api("/groups", admin, admin_mode: true), params: { statistics: true } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -313,8 +297,8 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context "when using skip_groups in request" do - it "returns all groups excluding skipped groups" do - get api("/groups", admin), params: { skip_groups: [group2.id] } + it "returns all groups excluding skipped groups", :aggregate_failures do + get api("/groups", admin, admin_mode: true), params: { skip_groups: [group2.id] } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -326,7 +310,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do context "when using all_available in request" do let(:response_groups) { json_response.map { |group| group['name'] } } - it "returns all groups you have access to" do + it "returns all groups you have access to", :aggregate_failures do public_group = create :group, :public get api("/groups", user1), params: { all_available: true } @@ -348,7 +332,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do subgroup.add_owner(user1) end - it "doesn't return subgroups" do + it "doesn't return subgroups", :aggregate_failures do get api("/groups", user1), params: { top_level_only: true } expect(response).to have_gitlab_http_status(:ok) @@ -373,7 +357,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do group5.add_owner(user1) end - it "sorts by name ascending by default" do + it "sorts by name ascending by default", :aggregate_failures do get api("/groups", user1) expect(response).to have_gitlab_http_status(:ok) @@ -382,7 +366,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(response_groups).to eq(groups_visible_to_user(user1).order(:name).pluck(:name)) end - it "sorts in descending order when passed" do + it "sorts in descending order when passed", :aggregate_failures do get api("/groups", user1), params: { sort: "desc" } expect(response).to have_gitlab_http_status(:ok) @@ -391,7 +375,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(response_groups).to eq(groups_visible_to_user(user1).order(name: :desc).pluck(:name)) end - it "sorts by path in order_by param" do + it "sorts by path in order_by param", :aggregate_failures do get api("/groups", user1), params: { order_by: "path" } expect(response).to have_gitlab_http_status(:ok) @@ -400,7 +384,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(response_groups).to eq(groups_visible_to_user(user1).order(:path).pluck(:name)) end - it "sorts by id in the order_by param" do + it "sorts by id in the order_by param", :aggregate_failures do get api("/groups", user1), params: { order_by: "id" } expect(response).to have_gitlab_http_status(:ok) @@ -409,7 +393,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(response_groups).to eq(groups_visible_to_user(user1).order(:id).pluck(:name)) end - it "sorts also by descending id with pagination fix" do + it "sorts also by descending id with pagination fix", :aggregate_failures do get api("/groups", user1), params: { order_by: "id", sort: "desc" } expect(response).to have_gitlab_http_status(:ok) @@ -418,7 +402,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(response_groups).to eq(groups_visible_to_user(user1).order(id: :desc).pluck(:name)) end - it "sorts identical keys by id for good pagination" do + it "sorts identical keys by id for good pagination", :aggregate_failures do get api("/groups", user1), params: { search: "same-name", order_by: "name" } expect(response).to have_gitlab_http_status(:ok) @@ -427,7 +411,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(response_groups_ids).to eq(Group.select { |group| group['name'] == 'same-name' }.map { |group| group['id'] }.sort) end - it "sorts descending identical keys by id for good pagination" do + it "sorts descending identical keys by id for good pagination", :aggregate_failures do get api("/groups", user1), params: { search: "same-name", order_by: "name", sort: "desc" } expect(response).to have_gitlab_http_status(:ok) @@ -449,7 +433,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do subject { get api('/groups', user1), params: params } - it 'sorts top level groups before subgroups with exact matches first' do + it 'sorts top level groups before subgroups with exact matches first', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:ok) @@ -462,7 +446,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do context 'when `search` parameter is not given' do let(:params) { { order_by: 'similarity' } } - it 'sorts items ordered by name' do + it 'sorts items ordered by name', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:ok) @@ -480,7 +464,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'when using owned in the request' do - it 'returns an array of groups the user owns' do + it 'returns an array of groups the user owns', :aggregate_failures do group1.add_maintainer(user2) get api('/groups', user2), params: { owned: true } @@ -503,7 +487,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'with min_access_level parameter' do - it 'returns an array of groups the user has at least master access' do + it 'returns an array of groups the user has at least master access', :aggregate_failures do get api('/groups', user2), params: { min_access_level: 40 } expect(response).to have_gitlab_http_status(:ok) @@ -512,24 +496,15 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(response_groups).to contain_exactly(group2.id, group3.id) end - context 'distinct count with present_groups_select_all feature flag' do + context 'distinct count' do subject { get api('/groups', user2), params: { min_access_level: 40 } } + # Prevent Rails from optimizing the count query and inadvertadly creating a poor performing databse query. + # https://gitlab.com/gitlab-org/gitlab/-/issues/368969 it 'counts with *' do count_sql = /#{Regexp.escape('SELECT count(*)')}/i expect { subject }.to make_queries_matching count_sql end - - context 'when present_groups_select_all feature flag is disabled' do - before do - stub_feature_flags(present_groups_select_all: false) - end - - it 'counts with count_column' do - count_sql = /#{Regexp.escape('SELECT count(count_column)')}/i - expect { subject }.to make_queries_matching count_sql - end - end end end end @@ -541,7 +516,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do subject { get api('/groups', user1), params: { search: group1.path } } - it 'finds also groups with full path matching search param' do + it 'finds also groups with full path matching search param', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:ok) @@ -587,7 +562,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(response).to have_gitlab_http_status(:not_found) end - it 'returns 200 for a public group' do + it 'returns 200 for a public group', :aggregate_failures do get api("/groups/#{group1.id}") expect(response).to have_gitlab_http_status(:ok) @@ -617,7 +592,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context "when authenticated as user" do - it "returns one of user1's groups" do + it "returns one of user1's groups", :aggregate_failures do project = create(:project, namespace: group2, path: 'Foo') create(:project_group_link, project: project, group: group1) group = create(:group) @@ -661,7 +636,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response['shared_projects'][0]['id']).to eq(project.id) end - it "returns one of user1's groups without projects when with_projects option is set to false" do + it "returns one of user1's groups without projects when with_projects option is set to false", :aggregate_failures do project = create(:project, namespace: group2, path: 'Foo') create(:project_group_link, project: project, group: group1) @@ -673,14 +648,14 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response).not_to include('runners_token') end - it "doesn't return runners_token if the user is not the owner of the group" do + it "doesn't return runners_token if the user is not the owner of the group", :aggregate_failures do get api("/groups/#{group1.id}", user3) expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to include('runners_token') end - it "returns runners_token if the user is the owner of the group" do + it "returns runners_token if the user is the owner of the group", :aggregate_failures do group1.add_owner(user3) get api("/groups/#{group1.id}", user3) @@ -720,8 +695,9 @@ RSpec.describe API::Groups, feature_category: :subgroups do .to contain_exactly(projects[:public].id, projects[:internal].id) end - it 'avoids N+1 queries with project links' do + it 'avoids N+1 queries with project links', :aggregate_failures do get api("/groups/#{group1.id}", user1) + expect(response).to have_gitlab_http_status(:ok) control_count = ActiveRecord::QueryRecorder.new do get api("/groups/#{group1.id}", user1) @@ -754,25 +730,25 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context "when authenticated as admin" do - it "returns any existing group" do - get api("/groups/#{group2.id}", admin) + it "returns any existing group", :aggregate_failures do + get api("/groups/#{group2.id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['name']).to eq(group2.name) end - it "returns information of the runners_token for the group" do - get api("/groups/#{group2.id}", admin) + it "returns information of the runners_token for the group", :aggregate_failures do + get api("/groups/#{group2.id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to include('runners_token') end - it "returns runners_token and no projects when with_projects option is set to false" do + it "returns runners_token and no projects when with_projects option is set to false", :aggregate_failures do project = create(:project, namespace: group2, path: 'Foo') create(:project_group_link, project: project, group: group1) - get api("/groups/#{group2.id}", admin), params: { with_projects: false } + get api("/groups/#{group2.id}", admin, admin_mode: true), params: { with_projects: false } expect(response).to have_gitlab_http_status(:ok) expect(json_response['projects']).to be_nil @@ -781,14 +757,14 @@ RSpec.describe API::Groups, feature_category: :subgroups do end it "does not return a non existing group" do - get api("/groups/#{non_existing_record_id}", admin) + get api("/groups/#{non_existing_record_id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end end context 'when using group path in URL' do - it 'returns any existing group' do + it 'returns any existing group', :aggregate_failures do get api("/groups/#{group1.path}", admin) expect(response).to have_gitlab_http_status(:ok) @@ -796,7 +772,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end it 'does not return a non existing group' do - get api('/groups/unknown', admin) + get api('/groups/unknown', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end @@ -826,7 +802,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end end - it 'limits projects and shared_projects' do + it 'limits projects and shared_projects', :aggregate_failures do get api("/groups/#{group1.id}") expect(json_response['projects'].count).to eq(limit) @@ -843,8 +819,8 @@ RSpec.describe API::Groups, feature_category: :subgroups do subject(:shared_with_groups) { json_response['shared_with_groups'].map { _1['group_id']} } context 'when authenticated as admin' do - it 'returns all groups that share the group' do - get api("/groups/#{shared_group.id}", admin) + it 'returns all groups that share the group', :aggregate_failures do + get api("/groups/#{shared_group.id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(shared_with_groups).to contain_exactly(group_link_1.shared_with_group_id, group_link_2.shared_with_group_id) @@ -852,7 +828,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'when unauthenticated' do - it 'returns only public groups that share the group' do + it 'returns only public groups that share the group', :aggregate_failures do get api("/groups/#{shared_group.id}") expect(response).to have_gitlab_http_status(:ok) @@ -861,7 +837,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'when authenticated as a member of a parent group that has shared the group' do - it 'returns private group if direct member' do + it 'returns private group if direct member', :aggregate_failures do group2_sub.add_guest(user3) get api("/groups/#{shared_group.id}", user3) @@ -870,7 +846,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(shared_with_groups).to contain_exactly(group_link_1.shared_with_group_id, group_link_2.shared_with_group_id) end - it 'returns private group if inherited member' do + it 'returns private group if inherited member', :aggregate_failures do inherited_guest_member = create(:user) group2.add_guest(inherited_guest_member) @@ -902,7 +878,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'when authenticated as the group owner' do - it 'updates the group' do + it 'updates the group', :aggregate_failures do workhorse_form_with_file( api("/groups/#{group1.id}", user1), method: :put, @@ -942,7 +918,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response['prevent_sharing_groups_outside_hierarchy']).to eq(true) end - it 'removes the group avatar' do + it 'removes the group avatar', :aggregate_failures do put api("/groups/#{group1.id}", user1), params: { avatar: '' } aggregate_failures "testing response" do @@ -952,7 +928,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end end - it 'does not update visibility_level if it is restricted' do + it 'does not update visibility_level if it is restricted', :aggregate_failures do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) put api("/groups/#{group1.id}", user1), params: { visibility: 'internal' } @@ -967,7 +943,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'for users who have the ability to update default_branch_protection' do - it 'updates the attribute' do + it 'updates the attribute', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:ok) @@ -976,7 +952,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'for users who does not have the ability to update default_branch_protection`' do - it 'does not update the attribute' do + it 'does not update the attribute', :aggregate_failures do allow(Ability).to receive(:allowed?).and_call_original allow(Ability).to receive(:allowed?).with(user1, :update_default_branch_protection, group1) { false } @@ -1016,21 +992,21 @@ RSpec.describe API::Groups, feature_category: :subgroups do group3.add_owner(user3) end - it 'does not change visibility when not requested' do + it 'does not change visibility when not requested', :aggregate_failures do put api("/groups/#{group3.id}", user3), params: { description: 'Bug #23083' } expect(response).to have_gitlab_http_status(:ok) expect(json_response['visibility']).to eq('public') end - it 'prevents making private a group containing public subgroups' do + it 'prevents making private a group containing public subgroups', :aggregate_failures do put api("/groups/#{group3.id}", user3), params: { visibility: 'private' } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']['visibility_level']).to contain_exactly('private is not allowed since there are sub-groups with higher visibility.') end - it 'does not update prevent_sharing_groups_outside_hierarchy' do + it 'does not update prevent_sharing_groups_outside_hierarchy', :aggregate_failures do put api("/groups/#{subgroup.id}", user3), params: { description: 'it works', prevent_sharing_groups_outside_hierarchy: true } expect(response).to have_gitlab_http_status(:ok) @@ -1042,17 +1018,17 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'when authenticated as the admin' do - it 'updates the group' do - put api("/groups/#{group1.id}", admin), params: { name: new_group_name } + it 'updates the group', :aggregate_failures do + put api("/groups/#{group1.id}", admin, admin_mode: true), params: { name: new_group_name } expect(response).to have_gitlab_http_status(:ok) expect(json_response['name']).to eq(new_group_name) end - it 'ignores visibility level restrictions' do + it 'ignores visibility level restrictions', :aggregate_failures do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) - put api("/groups/#{group1.id}", admin), params: { visibility: 'internal' } + put api("/groups/#{group1.id}", admin, admin_mode: true), params: { visibility: 'internal' } expect(response).to have_gitlab_http_status(:ok) expect(json_response['visibility']).to eq('internal') @@ -1094,7 +1070,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end end - it "returns the group's projects" do + it "returns the group's projects", :aggregate_failures do get api("/groups/#{group1.id}/projects", user1) expect(response).to have_gitlab_http_status(:ok) @@ -1106,7 +1082,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'and using archived' do - it "returns the group's archived projects" do + it "returns the group's archived projects", :aggregate_failures do get api("/groups/#{group1.id}/projects?archived=true", user1) expect(response).to have_gitlab_http_status(:ok) @@ -1116,7 +1092,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.map { |project| project['id'] }).to include(archived_project.id) end - it "returns the group's non-archived projects" do + it "returns the group's non-archived projects", :aggregate_failures do get api("/groups/#{group1.id}/projects?archived=false", user1) expect(response).to have_gitlab_http_status(:ok) @@ -1126,7 +1102,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.map { |project| project['id'] }).not_to include(archived_project.id) end - it "returns all of the group's projects" do + it "returns all of the group's projects", :aggregate_failures do get api("/groups/#{group1.id}/projects", user1) expect(response).to have_gitlab_http_status(:ok) @@ -1150,7 +1126,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do group_with_projects.add_owner(user1) end - it 'returns items based ordered by similarity' do + it 'returns items based ordered by similarity', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:ok) @@ -1166,7 +1142,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do params.delete(:search) end - it 'returns items ordered by name' do + it 'returns items ordered by name', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:ok) @@ -1179,7 +1155,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end end - it "returns the group's projects with simple representation" do + it "returns the group's projects with simple representation", :aggregate_failures do get api("/groups/#{group1.id}/projects", user1), params: { simple: true } expect(response).to have_gitlab_http_status(:ok) @@ -1190,7 +1166,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.first['visibility']).not_to be_present end - it "filters the groups projects" do + it "filters the groups projects", :aggregate_failures do public_project = create(:project, :public, path: 'test1', group: group1) get api("/groups/#{group1.id}/projects", user1), params: { visibility: 'public' } @@ -1202,7 +1178,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.first['name']).to eq(public_project.name) end - it "returns projects excluding shared" do + it "returns projects excluding shared", :aggregate_failures do create(:project_group_link, project: create(:project), group: group1) create(:project_group_link, project: create(:project), group: group1) create(:project_group_link, project: create(:project), group: group1) @@ -1227,7 +1203,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do group1.reload end - it "returns projects including those in subgroups" do + it "returns projects including those in subgroups", :aggregate_failures do get api("/groups/#{group1.id}/projects", user1), params: { include_subgroups: true } expect(response).to have_gitlab_http_status(:ok) @@ -1236,7 +1212,10 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.length).to eq(6) end - it 'avoids N+1 queries', :use_sql_query_cache, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/383788' do + it 'avoids N+1 queries', :aggregate_failures, :use_sql_query_cache, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/383788' do + get api("/groups/#{group1.id}/projects", user1), params: { include_subgroups: true } + expect(respone).to have_gitlab_http_status(:ok) + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do get api("/groups/#{group1.id}/projects", user1), params: { include_subgroups: true } end @@ -1250,7 +1229,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'when include_ancestor_groups is true' do - it 'returns ancestors groups projects' do + it 'returns ancestors groups projects', :aggregate_failures do subgroup = create(:group, parent: group1) subgroup_project = create(:project, group: subgroup) @@ -1275,7 +1254,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(response).to have_gitlab_http_status(:not_found) end - it "only returns projects to which user has access" do + it "only returns projects to which user has access", :aggregate_failures do project3.add_developer(user3) get api("/groups/#{group1.id}/projects", user3) @@ -1286,7 +1265,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.first['name']).to eq(project3.name) end - it 'only returns the projects owned by user' do + it 'only returns the projects owned by user', :aggregate_failures do project2.group.add_owner(user3) get api("/groups/#{project2.group.id}/projects", user3), params: { owned: true } @@ -1296,7 +1275,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.first['name']).to eq(project2.name) end - it 'only returns the projects starred by user' do + it 'only returns the projects starred by user', :aggregate_failures do user1.starred_projects = [project1] get api("/groups/#{group1.id}/projects", user1), params: { starred: true } @@ -1306,8 +1285,9 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.first['name']).to eq(project1.name) end - it 'avoids N+1 queries' do + it 'avoids N+1 queries', :aggregate_failures do get api("/groups/#{group1.id}/projects", user1) + expect(response).to have_gitlab_http_status(:ok) control_count = ActiveRecord::QueryRecorder.new do get api("/groups/#{group1.id}/projects", user1) @@ -1322,8 +1302,8 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context "when authenticated as admin" do - it "returns any existing group" do - get api("/groups/#{group2.id}/projects", admin) + it "returns any existing group", :aggregate_failures do + get api("/groups/#{group2.id}/projects", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -1332,15 +1312,15 @@ RSpec.describe API::Groups, feature_category: :subgroups do end it "does not return a non existing group" do - get api("/groups/#{non_existing_record_id}/projects", admin) + get api("/groups/#{non_existing_record_id}/projects", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end end context 'when using group path in URL' do - it 'returns any existing group' do - get api("/groups/#{group1.path}/projects", admin) + it 'returns any existing group', :aggregate_failures do + get api("/groups/#{group1.path}/projects", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -1349,7 +1329,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end it 'does not return a non existing group' do - get api('/groups/unknown/projects', admin) + get api('/groups/unknown/projects', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end @@ -1375,7 +1355,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'when authenticated as user' do - it 'returns the shared projects in the group' do + it 'returns the shared projects in the group', :aggregate_failures do get api(path, user1) expect(response).to have_gitlab_http_status(:ok) @@ -1386,7 +1366,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.first['visibility']).to be_present end - it 'returns shared projects with min access level or higher' do + it 'returns shared projects with min access level or higher', :aggregate_failures do user = create(:user) project2.add_guest(user) @@ -1399,7 +1379,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.first['id']).to eq(project4.id) end - it 'returns the shared projects of the group with simple representation' do + it 'returns the shared projects of the group with simple representation', :aggregate_failures do get api(path, user1), params: { simple: true } expect(response).to have_gitlab_http_status(:ok) @@ -1410,7 +1390,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.first['visibility']).not_to be_present end - it 'filters the shared projects in the group based on visibility' do + it 'filters the shared projects in the group based on visibility', :aggregate_failures do internal_project = create(:project, :internal, namespace: create(:group)) create(:project_group_link, project: internal_project, group: group1) @@ -1424,7 +1404,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.first['id']).to eq(internal_project.id) end - it 'filters the shared projects in the group based on search params' do + it 'filters the shared projects in the group based on search params', :aggregate_failures do get api(path, user1), params: { search: 'test_project' } expect(response).to have_gitlab_http_status(:ok) @@ -1434,7 +1414,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.first['id']).to eq(project4.id) end - it 'does not return the projects owned by the group' do + it 'does not return the projects owned by the group', :aggregate_failures do get api(path, user1) expect(response).to have_gitlab_http_status(:ok) @@ -1459,7 +1439,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(response).to have_gitlab_http_status(:not_found) end - it 'only returns shared projects to which user has access' do + it 'only returns shared projects to which user has access', :aggregate_failures do project4.add_developer(user3) get api(path, user3) @@ -1470,7 +1450,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response.first['id']).to eq(project4.id) end - it 'only returns the projects starred by user' do + it 'only returns the projects starred by user', :aggregate_failures do user1.starred_projects = [project2] get api(path, user1), params: { starred: true } @@ -1482,9 +1462,9 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context "when authenticated as admin" do - subject { get api(path, admin) } + subject { get api(path, admin, admin_mode: true) } - it "returns shared projects of an existing group" do + it "returns shared projects of an existing group", :aggregate_failures do subject expect(response).to have_gitlab_http_status(:ok) @@ -1504,7 +1484,10 @@ RSpec.describe API::Groups, feature_category: :subgroups do end end - it 'avoids N+1 queries' do + it 'avoids N+1 queries', :aggregate_failures, :use_sql_query_cache do + subject + expect(response).to have_gitlab_http_status(:ok) + control_count = ActiveRecord::QueryRecorder.new do subject end.count @@ -1520,8 +1503,8 @@ RSpec.describe API::Groups, feature_category: :subgroups do context 'when using group path in URL' do let(:path) { "/groups/#{group1.path}/projects/shared" } - it 'returns the right details' do - get api(path, admin) + it 'returns the right details', :aggregate_failures do + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -1531,7 +1514,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end it 'returns 404 for a non-existent group' do - get api('/groups/unknown/projects/shared', admin) + get api('/groups/unknown/projects/shared', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end @@ -1544,7 +1527,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do let!(:subgroup3) { create(:group, :private, parent: group2) } context 'when unauthenticated' do - it 'returns only public subgroups' do + it 'returns only public subgroups', :aggregate_failures do get api("/groups/#{group1.id}/subgroups") expect(response).to have_gitlab_http_status(:ok) @@ -1562,7 +1545,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'when statistics are requested' do - it 'does not include statistics' do + it 'does not include statistics', :aggregate_failures do get api("/groups/#{group1.id}/subgroups"), params: { statistics: true } expect(response).to have_gitlab_http_status(:ok) @@ -1575,7 +1558,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do context 'when authenticated as user' do context 'when user is not member of a public group' do - it 'returns no subgroups for the public group' do + it 'returns no subgroups for the public group', :aggregate_failures do get api("/groups/#{group1.id}/subgroups", user2) expect(response).to have_gitlab_http_status(:ok) @@ -1584,7 +1567,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'when using all_available in request' do - it 'returns public subgroups' do + it 'returns public subgroups', :aggregate_failures do get api("/groups/#{group1.id}/subgroups", user2), params: { all_available: true } expect(response).to have_gitlab_http_status(:ok) @@ -1609,7 +1592,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do group1.add_guest(user2) end - it 'returns private subgroups' do + it 'returns private subgroups', :aggregate_failures do get api("/groups/#{group1.id}/subgroups", user2) expect(response).to have_gitlab_http_status(:ok) @@ -1623,7 +1606,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'when using statistics in request' do - it 'does not include statistics' do + it 'does not include statistics', :aggregate_failures do get api("/groups/#{group1.id}/subgroups", user2), params: { statistics: true } expect(response).to have_gitlab_http_status(:ok) @@ -1638,7 +1621,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do group2.add_guest(user1) end - it 'returns subgroups' do + it 'returns subgroups', :aggregate_failures do get api("/groups/#{group2.id}/subgroups", user1) expect(response).to have_gitlab_http_status(:ok) @@ -1651,32 +1634,32 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'when authenticated as admin' do - it 'returns private subgroups of a public group' do - get api("/groups/#{group1.id}/subgroups", admin) + it 'returns private subgroups of a public group', :aggregate_failures do + get api("/groups/#{group1.id}/subgroups", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array expect(json_response.length).to eq(2) end - it 'returns subgroups of a private group' do - get api("/groups/#{group2.id}/subgroups", admin) + it 'returns subgroups of a private group', :aggregate_failures do + get api("/groups/#{group2.id}/subgroups", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array expect(json_response.length).to eq(1) end - it 'does not include statistics by default' do - get api("/groups/#{group1.id}/subgroups", admin) + it 'does not include statistics by default', :aggregate_failures do + get api("/groups/#{group1.id}/subgroups", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array expect(json_response.first).not_to include('statistics') end - it 'includes statistics if requested' do - get api("/groups/#{group1.id}/subgroups", admin), params: { statistics: true } + it 'includes statistics if requested', :aggregate_failures do + get api("/groups/#{group1.id}/subgroups", admin, admin_mode: true), params: { statistics: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array @@ -1700,7 +1683,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do let(:response_groups) { json_response.map { |group| group['name'] } } context 'when unauthenticated' do - it 'returns only public descendants' do + it 'returns only public descendants', :aggregate_failures do get api("/groups/#{group1.id}/descendant_groups") expect(response).to have_gitlab_http_status(:ok) @@ -1719,7 +1702,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do context 'when authenticated as user' do context 'when user is not member of a public group' do - it 'returns no descendants for the public group' do + it 'returns no descendants for the public group', :aggregate_failures do get api("/groups/#{group1.id}/descendant_groups", user2) expect(response).to have_gitlab_http_status(:ok) @@ -1728,7 +1711,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'when using all_available in request' do - it 'returns public descendants' do + it 'returns public descendants', :aggregate_failures do get api("/groups/#{group1.id}/descendant_groups", user2), params: { all_available: true } expect(response).to have_gitlab_http_status(:ok) @@ -1752,7 +1735,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do group1.add_guest(user2) end - it 'returns private descendants' do + it 'returns private descendants', :aggregate_failures do get api("/groups/#{group1.id}/descendant_groups", user2) expect(response).to have_gitlab_http_status(:ok) @@ -1763,7 +1746,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'when using statistics in request' do - it 'does not include statistics' do + it 'does not include statistics', :aggregate_failures do get api("/groups/#{group1.id}/descendant_groups", user2), params: { statistics: true } expect(response).to have_gitlab_http_status(:ok) @@ -1778,7 +1761,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do group2.add_guest(user1) end - it 'returns descendants' do + it 'returns descendants', :aggregate_failures do get api("/groups/#{group2.id}/descendant_groups", user1) expect(response).to have_gitlab_http_status(:ok) @@ -1790,32 +1773,32 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'when authenticated as admin' do - it 'returns private descendants of a public group' do - get api("/groups/#{group1.id}/descendant_groups", admin) + it 'returns private descendants of a public group', :aggregate_failures do + get api("/groups/#{group1.id}/descendant_groups", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array expect(json_response.length).to eq(3) end - it 'returns descendants of a private group' do - get api("/groups/#{group2.id}/descendant_groups", admin) + it 'returns descendants of a private group', :aggregate_failures do + get api("/groups/#{group2.id}/descendant_groups", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array expect(json_response.length).to eq(2) end - it 'does not include statistics by default' do - get api("/groups/#{group1.id}/descendant_groups", admin) + it 'does not include statistics by default', :aggregate_failures do + get api("/groups/#{group1.id}/descendant_groups", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array expect(json_response.first).not_to include('statistics') end - it 'includes statistics if requested' do - get api("/groups/#{group1.id}/descendant_groups", admin), params: { statistics: true } + it 'includes statistics if requested', :aggregate_failures do + get api("/groups/#{group1.id}/descendant_groups", admin, admin_mode: true), params: { statistics: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array @@ -1880,7 +1863,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context "when authenticated as user with group permissions" do - it "creates group" do + it "creates group", :aggregate_failures do group = attributes_for_group_api request_access_enabled: false post api("/groups", user3), params: group @@ -1893,7 +1876,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(json_response["visibility"]).to eq(Gitlab::VisibilityLevel.string_level(Gitlab::CurrentSettings.current_application_settings.default_group_visibility)) end - it "creates a nested group" do + it "creates a nested group", :aggregate_failures do parent = create(:group) parent.add_owner(user3) group = attributes_for_group_api parent_id: parent.id @@ -1926,7 +1909,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do subject { post api("/groups", user3), params: params } context 'for users who have the ability to create a group with `default_branch_protection`' do - it 'creates group with the specified branch protection level' do + it 'creates group with the specified branch protection level', :aggregate_failures do subject expect(response).to have_gitlab_http_status(:created) @@ -1935,7 +1918,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'for users who do not have the ability to create a group with `default_branch_protection`' do - it 'does not create the group with the specified branch protection level' do + it 'does not create the group with the specified branch protection level', :aggregate_failures do allow(Ability).to receive(:allowed?).and_call_original allow(Ability).to receive(:allowed?).with(user3, :create_group_with_default_branch_protection) { false } @@ -1947,7 +1930,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end end - it "does not create group, duplicate" do + it "does not create group, duplicate", :aggregate_failures do post api("/groups", user3), params: { name: 'Duplicate Test', path: group2.path } expect(response).to have_gitlab_http_status(:bad_request) @@ -2007,13 +1990,13 @@ RSpec.describe API::Groups, feature_category: :subgroups do context "when authenticated as admin" do it "removes any existing group" do - delete api("/groups/#{group2.id}", admin) + delete api("/groups/#{group2.id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:accepted) end it "does not remove a non existing group" do - delete api("/groups/#{non_existing_record_id}", admin) + delete api("/groups/#{non_existing_record_id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end @@ -2040,7 +2023,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do context "when authenticated as admin" do it "transfers project to group" do - post api("/groups/#{group1.id}/projects/#{project.id}", admin) + post api("/groups/#{group1.id}/projects/#{project.id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:created) end @@ -2048,7 +2031,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do context 'when using project path in URL' do context 'with a valid project path' do it "transfers project to group" do - post api("/groups/#{group1.id}/projects/#{project_path}", admin) + post api("/groups/#{group1.id}/projects/#{project_path}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:created) end @@ -2056,7 +2039,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do context 'with a non-existent project path' do it "does not transfer project to group" do - post api("/groups/#{group1.id}/projects/nogroup%2Fnoproject", admin) + post api("/groups/#{group1.id}/projects/nogroup%2Fnoproject", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end @@ -2066,7 +2049,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do context 'when using a group path in URL' do context 'with a valid group path' do it "transfers project to group" do - post api("/groups/#{group1.path}/projects/#{project_path}", admin) + post api("/groups/#{group1.path}/projects/#{project_path}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:created) end @@ -2074,7 +2057,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do context 'with a non-existent group path' do it "does not transfer project to group" do - post api("/groups/noexist/projects/#{project_path}", admin) + post api("/groups/noexist/projects/#{project_path}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end @@ -2183,7 +2166,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do context 'when promoting a subgroup to a root group' do shared_examples_for 'promotes the subgroup to a root group' do - it 'returns success' do + it 'returns success', :aggregate_failures do make_request(user) expect(response).to have_gitlab_http_status(:created) @@ -2207,7 +2190,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do let(:group) { create(:group) } let(:params) { { group_id: '' } } - it 'returns error' do + it 'returns error', :aggregate_failures do make_request(user) expect(response).to have_gitlab_http_status(:bad_request) @@ -2258,7 +2241,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end end - it 'returns error' do + it 'returns error', :aggregate_failures do make_request(user) expect(response).to have_gitlab_http_status(:bad_request) @@ -2267,7 +2250,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do end context 'when the transfer succceds' do - it 'returns success' do + it 'returns success', :aggregate_failures do make_request(user) expect(response).to have_gitlab_http_status(:created) @@ -2289,11 +2272,13 @@ RSpec.describe API::Groups, feature_category: :subgroups do describe "POST /groups/:id/share" do shared_examples 'shares group with group' do - it "shares group with group" do + let_it_be(:admin_mode) { false } + + it "shares group with group", :aggregate_failures do expires_at = 10.days.from_now.to_date expect do - post api("/groups/#{group.id}/share", user), params: { group_id: shared_with_group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at } + post api("/groups/#{group.id}/share", user, admin_mode: admin_mode), params: { group_id: shared_with_group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at } end.to change { group.shared_with_group_links.count }.by(1) expect(response).to have_gitlab_http_status(:created) @@ -2322,7 +2307,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do expect(response).to have_gitlab_http_status(:not_found) end - it "returns a 400 error when wrong params passed" do + it "returns a 400 error when wrong params passed", :aggregate_failures do post api("/groups/#{group.id}/share", user), params: { group_id: shared_with_group.id, group_access: non_existing_record_access_level } expect(response).to have_gitlab_http_status(:bad_request) @@ -2375,15 +2360,18 @@ RSpec.describe API::Groups, feature_category: :subgroups do let(:user) { admin } let(:group) { create(:group) } let(:shared_with_group) { create(:group) } + let(:admin_mode) { true } end end end describe 'DELETE /groups/:id/share/:group_id' do shared_examples 'deletes group share' do - it 'deletes a group share' do + let_it_be(:admin_mode) { false } + + it 'deletes a group share', :aggregate_failures do expect do - delete api("/groups/#{shared_group.id}/share/#{shared_with_group.id}", user) + delete api("/groups/#{shared_group.id}/share/#{shared_with_group.id}", user, admin_mode: admin_mode) expect(response).to have_gitlab_http_status(:no_content) expect(shared_group.shared_with_group_links).to be_empty @@ -2432,7 +2420,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do create(:group_group_link, shared_group: group1, shared_with_group: group_a) end - it 'does not remove group share' do + it 'does not remove group share', :aggregate_failures do expect do delete api("/groups/#{group1.id}/share/#{group_a.id}", user4) @@ -2452,6 +2440,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do let(:user) { admin } let(:shared_group) { group2 } let(:shared_with_group) { group_b } + let(:admin_mode) { true } end end end diff --git a/spec/requests/api/helm_packages_spec.rb b/spec/requests/api/helm_packages_spec.rb index 584f6e3c7d4..d6afd6f86ff 100644 --- a/spec/requests/api/helm_packages_spec.rb +++ b/spec/requests/api/helm_packages_spec.rb @@ -17,7 +17,15 @@ RSpec.describe API::HelmPackages, feature_category: :package_registry do let_it_be(:package_file2_2) { create(:helm_package_file, package: package2, file_sha256: 'file2', file_name: 'filename2.tgz', channel: 'test', description: 'hello from test channel') } let_it_be(:other_package) { create(:npm_package, project: project) } - let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_helm_user' } } + let(:snowplow_gitlab_standard_context) { snowplow_context } + + def snowplow_context(user_role: :developer) + if user_role == :anonymous + { project: project, namespace: project.namespace, property: 'i_package_helm_user' } + else + { project: project, namespace: project.namespace, property: 'i_package_helm_user', user: user } + end + end describe 'GET /api/v4/projects/:id/packages/helm/:channel/index.yaml' do let(:project_id) { project.id } @@ -65,6 +73,7 @@ RSpec.describe API::HelmPackages, feature_category: :package_registry do with_them do let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, personal_access_token.token) } + let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) } before do project.update!(visibility: visibility.to_s) @@ -75,6 +84,8 @@ RSpec.describe API::HelmPackages, feature_category: :package_registry do end context 'with access to package registry for everyone' do + let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: :anonymous) } + before do project.update!(visibility: Gitlab::VisibilityLevel::PRIVATE) project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC) @@ -116,6 +127,7 @@ RSpec.describe API::HelmPackages, feature_category: :package_registry do with_them do let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, personal_access_token.token) } let(:headers) { user_headers.merge(workhorse_headers) } + let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) } before do project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s)) @@ -178,6 +190,7 @@ RSpec.describe API::HelmPackages, feature_category: :package_registry do with_them do let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, personal_access_token.token) } let(:headers) { user_headers.merge(workhorse_headers) } + let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) } before do project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s)) diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 38275ce0057..0be9df41e8f 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' require 'raven/transports/dummy' require_relative '../../../config/initializers/sentry' -RSpec.describe API::Helpers, :enable_admin_mode, feature_category: :authentication_and_authorization do +RSpec.describe API::Helpers, :enable_admin_mode, feature_category: :system_access do include API::APIGuard::HelperMethods include described_class include TermsHelper diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb index 0d75bb94144..9b5ae72526c 100644 --- a/spec/requests/api/import_github_spec.rb +++ b/spec/requests/api/import_github_spec.rb @@ -174,72 +174,54 @@ RSpec.describe API::ImportGithub, feature_category: :importers do let_it_be(:user) { create(:user) } let(:params) { { personal_access_token: token } } - context 'when feature github_import_gists is enabled' do + context 'when gists import was started' do before do - stub_feature_flags(github_import_gists: true) + allow(Import::Github::GistsImportService) + .to receive(:new).with(user, client, access_params) + .and_return(double(execute: { status: :success })) end - context 'when gists import was started' do - before do - allow(Import::Github::GistsImportService) - .to receive(:new).with(user, client, access_params) - .and_return(double(execute: { status: :success })) - end - - it 'returns 202' do - post api('/import/github/gists', user), params: params + it 'returns 202' do + post api('/import/github/gists', user), params: params - expect(response).to have_gitlab_http_status(:accepted) - end + expect(response).to have_gitlab_http_status(:accepted) end + end - context 'when gists import is in progress' do - before do - allow(Import::Github::GistsImportService) - .to receive(:new).with(user, client, access_params) - .and_return(double(execute: { status: :error, message: 'Import already in progress', http_status: :unprocessable_entity })) - end - - it 'returns 422 error' do - post api('/import/github/gists', user), params: params - - expect(response).to have_gitlab_http_status(:unprocessable_entity) - expect(json_response['errors']).to eq('Import already in progress') - end + context 'when gists import is in progress' do + before do + allow(Import::Github::GistsImportService) + .to receive(:new).with(user, client, access_params) + .and_return(double(execute: { status: :error, message: 'Import already in progress', http_status: :unprocessable_entity })) end - context 'when unauthenticated user' do - it 'returns 403 error' do - post api('/import/github/gists'), params: params + it 'returns 422 error' do + post api('/import/github/gists', user), params: params - expect(response).to have_gitlab_http_status(:unauthorized) - end + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response['errors']).to eq('Import already in progress') end + end - context 'when rate limit reached' do - before do - allow(Import::Github::GistsImportService) - .to receive(:new).with(user, client, access_params) - .and_raise(Gitlab::GithubImport::RateLimitError) - end - - it 'returns 429 error' do - post api('/import/github/gists', user), params: params + context 'when unauthenticated user' do + it 'returns 403 error' do + post api('/import/github/gists'), params: params - expect(response).to have_gitlab_http_status(:too_many_requests) - end + expect(response).to have_gitlab_http_status(:unauthorized) end end - context 'when feature github_import_gists is disabled' do + context 'when rate limit reached' do before do - stub_feature_flags(github_import_gists: false) + allow(Import::Github::GistsImportService) + .to receive(:new).with(user, client, access_params) + .and_raise(Gitlab::GithubImport::RateLimitError) end - it 'returns 404 error' do + it 'returns 429 error' do post api('/import/github/gists', user), params: params - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(:too_many_requests) end end end diff --git a/spec/requests/api/integrations/slack/events_spec.rb b/spec/requests/api/integrations/slack/events_spec.rb new file mode 100644 index 00000000000..438715db4f0 --- /dev/null +++ b/spec/requests/api/integrations/slack/events_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Integrations::Slack::Events, feature_category: :integrations do + describe 'POST /integrations/slack/events' do + let_it_be(:slack_installation) { create(:slack_integration) } + + let(:params) { {} } + let(:headers) do + { + ::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER => Time.current.to_i.to_s, + ::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER => 'mock_verified_signature' + } + end + + before do + allow(ActiveSupport::SecurityUtils).to receive(:secure_compare) do |signature| + signature == 'mock_verified_signature' + end + + stub_application_setting(slack_app_signing_secret: 'mock_key') + end + + subject { post api('/integrations/slack/events'), params: params, headers: headers } + + it_behaves_like 'Slack request verification' + + context 'when type param is unknown' do + let(:params) do + { type: 'unknown_type' } + end + + it 'generates a tracked error' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).once + + subject + + expect(response).to have_gitlab_http_status(:no_content) + expect(response.body).to be_empty + end + end + + context 'when type param is url_verification' do + let(:params) do + { + type: 'url_verification', + challenge: '3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P' + } + end + + it 'responds in-request with the challenge' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq({ 'challenge' => '3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P' }) + end + end + + context 'when event.type param is app_home_opened' do + let(:params) do + { + type: 'event_callback', + team_id: slack_installation.team_id, + event_id: 'Ev03SA75UJKB', + event: { + type: 'app_home_opened', + user: 'U0123ABCDEF' + } + } + end + + it 'calls the Slack API (integration-style test)', :sidekiq_inline, :clean_gitlab_redis_shared_state do + api_url = "#{Slack::API::BASE_URL}/views.publish" + + stub_request(:post, api_url) + .to_return( + status: 200, + body: { ok: true }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + + subject + + expect(WebMock).to have_requested(:post, api_url) + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to eq('{}') + end + end + end +end diff --git a/spec/requests/api/integrations/slack/interactions_spec.rb b/spec/requests/api/integrations/slack/interactions_spec.rb new file mode 100644 index 00000000000..35a96be75e0 --- /dev/null +++ b/spec/requests/api/integrations/slack/interactions_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Integrations::Slack::Interactions, feature_category: :integrations do + describe 'POST /integrations/slack/interactions' do + let_it_be(:slack_installation) { create(:slack_integration) } + + let(:payload) { {} } + let(:params) { { payload: Gitlab::Json.dump(payload) } } + + let(:headers) do + { + ::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER => Time.current.to_i.to_s, + ::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER => 'mock_verified_signature' + } + end + + before do + allow(ActiveSupport::SecurityUtils).to receive(:secure_compare) do |signature| + signature == 'mock_verified_signature' + end + + stub_application_setting(slack_app_signing_secret: 'mock_key') + end + + subject { post api('/integrations/slack/interactions'), params: params, headers: headers } + + it_behaves_like 'Slack request verification' + + context 'when type param is unknown' do + let(:payload) do + { type: 'unknown_type' } + end + + it 'generates a tracked error' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).once + + subject + + expect(response).to have_gitlab_http_status(:no_content) + expect(response.body).to be_empty + end + end + + context 'when event.type param is view_closed' do + let(:payload) do + { + type: 'view_closed', + team_id: slack_installation.team_id, + event: { + type: 'view_closed', + user: 'U0123ABCDEF' + } + } + end + + it 'calls the Slack Interactivity Service' do + expect_next_instance_of(::Integrations::SlackInteractionService) do |service| + expect(service).to receive(:execute).and_return(ServiceResponse.success) + end + + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end + end +end diff --git a/spec/requests/api/integrations/slack/options_spec.rb b/spec/requests/api/integrations/slack/options_spec.rb new file mode 100644 index 00000000000..eef993d0329 --- /dev/null +++ b/spec/requests/api/integrations/slack/options_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Integrations::Slack::Options, feature_category: :integrations do + describe 'POST /integrations/slack/options' do + let_it_be(:slack_installation) { create(:slack_integration) } + + let(:payload) { {} } + let(:params) { { payload: Gitlab::Json.dump(payload) } } + + let(:headers) do + { + ::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER => Time.current.to_i.to_s, + ::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER => 'mock_verified_signature' + } + end + + before do + allow(ActiveSupport::SecurityUtils).to receive(:secure_compare) do |signature| + signature == 'mock_verified_signature' + end + + stub_application_setting(slack_app_signing_secret: 'mock_key') + end + + subject(:post_to_slack_api) { post api('/integrations/slack/options'), params: params, headers: headers } + + it_behaves_like 'Slack request verification' + + context 'when type param is unknown' do + let(:payload) do + { action_id: 'unknown_action' } + end + + it 'generates a tracked error' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).once + + post_to_slack_api + + expect(response).to have_gitlab_http_status(:no_content) + expect(response.body).to be_empty + end + end + + context 'when action_id param is assignee' do + let(:payload) do + { + action_id: 'assignee' + } + end + + it 'calls the Slack Interactivity Service' do + expect_next_instance_of(::Integrations::SlackOptionService) do |service| + expect(service).to receive(:execute).and_return(ServiceResponse.success) + end + + post_to_slack_api + + expect(response).to have_gitlab_http_status(:ok) + end + end + end +end diff --git a/spec/requests/api/integrations_spec.rb b/spec/requests/api/integrations_spec.rb index c35b9bab0ec..8d348dc0a54 100644 --- a/spec/requests/api/integrations_spec.rb +++ b/spec/requests/api/integrations_spec.rb @@ -10,14 +10,6 @@ RSpec.describe API::Integrations, feature_category: :integrations do create(:project, creator_id: user.id, namespace: user.namespace) end - # The API supports all integrations except the GitLab Slack Application - # integration; this integration must be installed via the UI. - def self.integration_names - names = Integration.available_integration_names - names.delete(Integrations::GitlabSlackApplication.to_param) if Gitlab.ee? - names - end - %w[integrations services].each do |endpoint| describe "GET /projects/:id/#{endpoint}" do it 'returns authentication error when unauthenticated' do @@ -51,9 +43,19 @@ RSpec.describe API::Integrations, feature_category: :integrations do end end - integration_names.each do |integration| + where(:integration) do + # The API supports all integrations except the GitLab Slack Application + # integration; this integration must be installed via the UI. + names = Integration.available_integration_names + names.delete(Integrations::GitlabSlackApplication.to_param) if Gitlab.ee? + names - %w[shimo zentao] + end + + with_them do + integration = params[:integration] + describe "PUT /projects/:id/#{endpoint}/#{integration.dasherize}" do - include_context integration + include_context 'with integration' # NOTE: Some attributes are not supported for PUT requests, even though they probably should be. # We can fix these manually, or with a generic approach like https://gitlab.com/gitlab-org/gitlab/-/issues/348208 @@ -62,7 +64,7 @@ RSpec.describe API::Integrations, feature_category: :integrations do datadog: %i[archive_trace_events], discord: %i[branches_to_be_notified notify_only_broken_pipelines], hangouts_chat: %i[notify_only_broken_pipelines], - jira: %i[issues_enabled project_key vulnerabilities_enabled vulnerabilities_issuetype], + jira: %i[issues_enabled project_key jira_issue_regex jira_issue_prefix vulnerabilities_enabled vulnerabilities_issuetype], mattermost: %i[deployment_channel labels_to_be_notified], mock_ci: %i[enable_ssl_verification], prometheus: %i[manual_configuration], @@ -119,7 +121,7 @@ RSpec.describe API::Integrations, feature_category: :integrations do end describe "DELETE /projects/:id/#{endpoint}/#{integration.dasherize}" do - include_context integration + include_context 'with integration' before do initialize_integration(integration) @@ -135,7 +137,7 @@ RSpec.describe API::Integrations, feature_category: :integrations do end describe "GET /projects/:id/#{endpoint}/#{integration.dasherize}" do - include_context integration + include_context 'with integration' let!(:initialized_integration) { initialize_integration(integration, active: true) } @@ -367,7 +369,7 @@ RSpec.describe API::Integrations, feature_category: :integrations do describe 'Jira integration' do let(:integration_name) { 'jira' } let(:params) do - { url: 'https://jira.example.com', username: 'username', password: 'password' } + { url: 'https://jira.example.com', username: 'username', password: 'password', jira_auth_type: 0 } end before do @@ -426,4 +428,28 @@ RSpec.describe API::Integrations, feature_category: :integrations do expect(response_keys).not_to include(*integration.secret_fields) end end + + describe 'POST /slack/trigger' do + before_all do + create(:gitlab_slack_application_integration, project: project) + end + + before do + stub_application_setting(slack_app_verification_token: 'token') + end + + it 'returns status 200' do + post api('/slack/trigger'), params: { token: 'token', text: 'help' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['response_type']).to eq("ephemeral") + end + + it 'returns status 404 when token is invalid' do + post api('/slack/trigger'), params: { token: 'invalid', text: 'foo' } + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['response_type']).to be_blank + end + end end diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index ca32271f573..6414b1efe6a 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Internal::Base, feature_category: :authentication_and_authorization do +RSpec.describe API::Internal::Base, feature_category: :system_access do include GitlabShellHelpers include APIInternalBaseHelpers @@ -10,6 +10,9 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author let_it_be(:project, reload: true) { create(:project, :repository, :wiki_repo) } let_it_be(:personal_snippet) { create(:personal_snippet, :repository, author: user) } let_it_be(:project_snippet) { create(:project_snippet, :repository, author: user, project: project) } + let_it_be(:max_pat_access_token_lifetime) do + PersonalAccessToken::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS.days.from_now.to_date.freeze + end let(:key) { create(:key, user: user) } let(:secret_token) { Gitlab::Shell.secret_token } @@ -194,39 +197,68 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author expect(json_response['message']).to match(/\AInvalid scope: 'badscope'. Valid scopes are: /) end - it 'returns a token without expiry when the expires_at parameter is missing' do - token_size = (PersonalAccessToken.token_prefix || '').size + 20 + it 'returns a token with expiry when it receives a valid expires_at parameter' do + freeze_time do + token_size = (PersonalAccessToken.token_prefix || '').size + 20 + + post api('/internal/personal_access_token'), + params: { + key_id: key.id, + name: 'newtoken', + scopes: %w(read_api read_repository), + expires_at: max_pat_access_token_lifetime + }, + headers: gitlab_shell_internal_api_request_header - post api('/internal/personal_access_token'), - params: { - key_id: key.id, - name: 'newtoken', - scopes: %w(read_api read_repository) - }, - headers: gitlab_shell_internal_api_request_header + expect(json_response['success']).to be_truthy + expect(json_response['token']).to match(/\A\S{#{token_size}}\z/) + expect(json_response['scopes']).to match_array(%w(read_api read_repository)) + expect(json_response['expires_at']).to eq(max_pat_access_token_lifetime.iso8601) + end + end - expect(json_response['success']).to be_truthy - expect(json_response['token']).to match(/\A\S{#{token_size}}\z/) - expect(json_response['scopes']).to match_array(%w(read_api read_repository)) - expect(json_response['expires_at']).to be_nil + context 'when default_pat_expiration feature flag is true' do + it 'returns token with expiry as PersonalAccessToken::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS' do + freeze_time do + token_size = (PersonalAccessToken.token_prefix || '').size + 20 + + post api('/internal/personal_access_token'), + params: { + key_id: key.id, + name: 'newtoken', + scopes: %w(read_api read_repository) + }, + headers: gitlab_shell_internal_api_request_header + + expect(json_response['success']).to be_truthy + expect(json_response['token']).to match(/\A\S{#{token_size}}\z/) + expect(json_response['scopes']).to match_array(%w(read_api read_repository)) + expect(json_response['expires_at']).to eq(max_pat_access_token_lifetime.iso8601) + end + end end - it 'returns a token with expiry when it receives a valid expires_at parameter' do - token_size = (PersonalAccessToken.token_prefix || '').size + 20 + context 'when default_pat_expiration feature flag is false' do + before do + stub_feature_flags(default_pat_expiration: false) + end - post api('/internal/personal_access_token'), - params: { - key_id: key.id, - name: 'newtoken', - scopes: %w(read_api read_repository), - expires_at: '9001-11-17' - }, - headers: gitlab_shell_internal_api_request_header + it 'uses nil expiration value' do + token_size = (PersonalAccessToken.token_prefix || '').size + 20 + + post api('/internal/personal_access_token'), + params: { + key_id: key.id, + name: 'newtoken', + scopes: %w(read_api read_repository) + }, + headers: gitlab_shell_internal_api_request_header - expect(json_response['success']).to be_truthy - expect(json_response['token']).to match(/\A\S{#{token_size}}\z/) - expect(json_response['scopes']).to match_array(%w(read_api read_repository)) - expect(json_response['expires_at']).to eq('9001-11-17') + expect(json_response['success']).to be_truthy + expect(json_response['token']).to match(/\A\S{#{token_size}}\z/) + expect(json_response['scopes']).to match_array(%w(read_api read_repository)) + expect(json_response['expires_at']).to be_nil + end end end @@ -514,7 +546,7 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author expect(json_response["gl_repository"]).to eq("wiki-#{project.id}") expect(json_response["gl_key_type"]).to eq("key") expect(json_response["gl_key_id"]).to eq(key.id) - expect(user.reload.last_activity_on).to be_nil + expect(user.reload.last_activity_on).to eql(Date.today) end it_behaves_like 'sets hook env' do @@ -553,7 +585,7 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author expect(json_response["status"]).to be_truthy expect(json_response["gl_project_path"]).to eq(personal_snippet.repository.full_path) expect(json_response["gl_repository"]).to eq("snippet-#{personal_snippet.id}") - expect(user.reload.last_activity_on).to be_nil + expect(user.reload.last_activity_on).to eql(Date.today) end it_behaves_like 'sets hook env' do @@ -585,7 +617,7 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author expect(json_response["status"]).to be_truthy expect(json_response["gl_project_path"]).to eq(project_snippet.repository.full_path) expect(json_response["gl_repository"]).to eq("snippet-#{project_snippet.id}") - expect(user.reload.last_activity_on).to be_nil + expect(user.reload.last_activity_on).to eql(Date.today) end it_behaves_like 'sets hook env' do @@ -703,7 +735,7 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path) expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage)) expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage)) - expect(user.reload.last_activity_on).to be_nil + expect(user.reload.last_activity_on).to eql(Date.today) end it_behaves_like 'rate limited request' do @@ -862,7 +894,7 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author expect(json_response['status']).to be_truthy expect(json_response['payload']).to eql(payload) expect(json_response['gl_console_messages']).to eql(console_messages) - expect(user.reload.last_activity_on).to be_nil + expect(user.reload.last_activity_on).to eql(Date.today) end end end diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb index be76e55269a..c07382a6e04 100644 --- a/spec/requests/api/internal/kubernetes_spec.rb +++ b/spec/requests/api/internal/kubernetes_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Internal::Kubernetes, feature_category: :kubernetes_management do +RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_management do let(:jwt_auth_headers) do jwt_token = JWT.encode({ 'iss' => Gitlab::Kas::JWT_ISSUER }, Gitlab::Kas.secret, 'HS256') @@ -59,12 +59,29 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :kubernetes_manageme end end + shared_examples 'error handling' do + let!(:agent_token) { create(:cluster_agent_token) } + + # this test verifies fix for an issue where AgentToken passed in Authorization + # header broke error handling in the api_helpers.rb. It can be removed after + # https://gitlab.com/gitlab-org/gitlab/-/issues/406582 is done + it 'returns correct error for the endpoint' do + allow(Gitlab::Kas).to receive(:verify_api_request).and_raise(StandardError.new('Unexpected Error')) + + send_request(headers: { 'Authorization' => "Bearer #{agent_token.token}" }) + + expect(response).to have_gitlab_http_status(:internal_server_error) + expect(response.body).to include("Unexpected Error") + end + end + describe 'POST /internal/kubernetes/usage_metrics', :clean_gitlab_redis_shared_state do def send_request(headers: {}, params: {}) post api('/internal/kubernetes/usage_metrics'), params: params, headers: headers.reverse_merge(jwt_auth_headers) end include_examples 'authorization' + include_examples 'error handling' context 'is authenticated for an agent' do let!(:agent_token) { create(:cluster_agent_token) } @@ -147,19 +164,30 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :kubernetes_manageme projects: [ { id: project.full_path, default_namespace: 'staging' } ] + }, + user_access: { + groups: [ + { id: group.full_path } + ], + projects: [ + { id: project.full_path } + ] } } end include_examples 'authorization' + include_examples 'error handling' context 'agent exists' do it 'configures the agent and returns a 204' do send_request(params: { agent_id: agent.id, agent_config: config }) expect(response).to have_gitlab_http_status(:no_content) - expect(agent.authorized_groups).to contain_exactly(group) - expect(agent.authorized_projects).to contain_exactly(project) + expect(agent.ci_access_authorized_groups).to contain_exactly(group) + expect(agent.ci_access_authorized_projects).to contain_exactly(project) + expect(agent.user_access_authorized_groups).to contain_exactly(group) + expect(agent.user_access_authorized_projects).to contain_exactly(project) end end @@ -179,6 +207,7 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :kubernetes_manageme include_examples 'authorization' include_examples 'agent authentication' + include_examples 'error handling' context 'an agent is found' do let!(:agent_token) { create(:cluster_agent_token) } @@ -223,6 +252,7 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :kubernetes_manageme include_examples 'authorization' include_examples 'agent authentication' + include_examples 'error handling' context 'an agent is found' do let_it_be(:agent_token) { create(:cluster_agent_token) } @@ -306,4 +336,145 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :kubernetes_manageme end end end + + describe 'POST /internal/kubernetes/authorize_proxy_user', :clean_gitlab_redis_sessions do + include SessionHelpers + + def send_request(headers: {}, params: {}) + post api('/internal/kubernetes/authorize_proxy_user'), params: params, headers: headers.reverse_merge(jwt_auth_headers) + end + + def stub_user_session(user, csrf_token) + stub_session( + { + 'warden.user.user.key' => [[user.id], user.authenticatable_salt], + '_csrf_token' => csrf_token + } + ) + end + + def stub_user_session_with_no_user_id(user, csrf_token) + stub_session( + { + 'warden.user.user.key' => [[nil], user.authenticatable_salt], + '_csrf_token' => csrf_token + } + ) + end + + def mask_token(encoded_token) + controller = ActionController::Base.new + raw_token = controller.send(:decode_csrf_token, encoded_token) + controller.send(:mask_token, raw_token) + end + + def new_token + ActionController::Base.new.send(:generate_csrf_token) + end + + let_it_be(:organization) { create(:group) } + let_it_be(:configuration_project) { create(:project, group: organization) } + let_it_be(:agent) { create(:cluster_agent, name: 'the-agent', project: configuration_project) } + let_it_be(:another_agent) { create(:cluster_agent) } + let_it_be(:deployment_project) { create(:project, group: organization) } + let_it_be(:deployment_group) { create(:group, parent: organization) } + + let(:user_access_config) do + { + 'user_access' => { + 'access_as' => { 'agent' => {} }, + 'projects' => [{ 'id' => deployment_project.full_path }], + 'groups' => [{ 'id' => deployment_group.full_path }] + } + } + end + + let(:user) { create(:user) } + + before do + allow(::Gitlab::Kas).to receive(:enabled?).and_return true + Clusters::Agents::Authorizations::UserAccess::RefreshService.new(agent, config: user_access_config).execute + end + + it 'returns 400 when cookie is invalid' do + send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: '123', csrf_token: mask_token(new_token) }) + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns 401 when session is not found' do + access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id('abc') + send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(new_token) }) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'returns 401 when CSRF token does not match' do + public_id = stub_user_session(user, new_token) + access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) + send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(new_token) }) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'returns 404 for non-existent agent' do + token = new_token + public_id = stub_user_session(user, token) + access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) + send_request(params: { agent_id: non_existing_record_id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) }) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 403 when user has no access' do + token = new_token + public_id = stub_user_session(user, token) + access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) + send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) }) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'returns 200 when user has access' do + deployment_project.add_member(user, :developer) + token = new_token + public_id = stub_user_session(user, token) + access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) + send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) }) + + expect(response).to have_gitlab_http_status(:success) + end + + it 'returns 401 when user has valid KAS cookie and CSRF token but has no access to requested agent' do + deployment_project.add_member(user, :developer) + token = new_token + public_id = stub_user_session(user, token) + access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) + send_request(params: { agent_id: another_agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) }) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'returns 401 when global flag is disabled' do + stub_feature_flags(kas_user_access: false) + + deployment_project.add_member(user, :developer) + token = new_token + public_id = stub_user_session(user, token) + access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) + send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) }) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'returns 401 when user id is not found in session' do + deployment_project.add_member(user, :developer) + token = new_token + public_id = stub_user_session_with_no_user_id(user, token) + access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id) + send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) }) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end end diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb index 56f1089843b..1006319eabf 100644 --- a/spec/requests/api/internal/pages_spec.rb +++ b/spec/requests/api/internal/pages_spec.rb @@ -3,193 +3,97 @@ require 'spec_helper' RSpec.describe API::Internal::Pages, feature_category: :pages do - let(:auth_headers) do - jwt_token = JWT.encode({ 'iss' => 'gitlab-pages' }, Gitlab::Pages.secret, 'HS256') - { Gitlab::Pages::INTERNAL_API_REQUEST_HEADER => jwt_token } + let_it_be(:group) { create(:group) } + let_it_be_with_reload(:project) { create(:project, group: group) } + + let(:auth_header) do + { + Gitlab::Pages::INTERNAL_API_REQUEST_HEADER => JWT.encode( + { 'iss' => 'gitlab-pages' }, + Gitlab::Pages.secret, 'HS256') + } end - let(:pages_secret) { SecureRandom.random_bytes(Gitlab::Pages::SECRET_LENGTH) } - before do - allow(Gitlab::Pages).to receive(:secret).and_return(pages_secret) + allow(Gitlab::Pages) + .to receive(:secret) + .and_return(SecureRandom.random_bytes(Gitlab::Pages::SECRET_LENGTH)) + stub_pages_object_storage(::Pages::DeploymentUploader) end - describe "GET /internal/pages/status" do - def query_enabled(headers = {}) - get api("/internal/pages/status"), headers: headers - end - + describe 'GET /internal/pages/status' do it 'responds with 401 Unauthorized' do - query_enabled + get api('/internal/pages/status') expect(response).to have_gitlab_http_status(:unauthorized) end it 'responds with 204 no content' do - query_enabled(auth_headers) + get api('/internal/pages/status'), headers: auth_header expect(response).to have_gitlab_http_status(:no_content) expect(response.body).to be_empty end end - describe "GET /internal/pages" do - def query_host(host, headers = {}) - get api("/internal/pages"), headers: headers, params: { host: host } - end - - around do |example| - freeze_time do - example.run - end - end - - context 'not authenticated' do + describe 'GET /internal/pages' do + context 'when not authenticated' do it 'responds with 401 Unauthorized' do - query_host('pages.gitlab.io') + get api('/internal/pages') expect(response).to have_gitlab_http_status(:unauthorized) end end - context 'authenticated' do - def query_host(host) - jwt_token = JWT.encode({ 'iss' => 'gitlab-pages' }, Gitlab::Pages.secret, 'HS256') - headers = { Gitlab::Pages::INTERNAL_API_REQUEST_HEADER => jwt_token } - - super(host, headers) + context 'when authenticated' do + before do + project.update_pages_deployment!(create(:pages_deployment, project: project)) end - def deploy_pages(project) - deployment = create(:pages_deployment, project: project) - project.mark_pages_as_deployed - project.update_pages_deployment!(deployment) + around do |example| + freeze_time do + example.run + end end - context 'domain does not exist' do + context 'when domain does not exist' do it 'responds with 204 no content' do - query_host('pages.gitlab.io') + get api('/internal/pages'), headers: auth_header, params: { host: 'any-domain.gitlab.io' } expect(response).to have_gitlab_http_status(:no_content) expect(response.body).to be_empty end end - context 'serverless domain' do - let(:namespace) { create(:namespace, name: 'gitlab-org') } - let(:project) { create(:project, namespace: namespace, name: 'gitlab-ce') } - let(:environment) { create(:environment, project: project) } - let(:pages_domain) { create(:pages_domain, domain: 'serverless.gitlab.io') } - let(:knative_without_ingress) { create(:clusters_applications_knative) } - let(:knative_with_ingress) { create(:clusters_applications_knative, external_ip: '10.0.0.1') } - - context 'without a knative ingress gateway IP' do - let!(:serverless_domain_cluster) do - create( - :serverless_domain_cluster, - uuid: 'abcdef12345678', - pages_domain: pages_domain, - knative: knative_without_ingress - ) - end - - let(:serverless_domain) do - create( - :serverless_domain, - serverless_domain_cluster: serverless_domain_cluster, - environment: environment - ) - end - - it 'responds with 204 no content' do - query_host(serverless_domain.uri.host) - - expect(response).to have_gitlab_http_status(:no_content) - expect(response.body).to be_empty - end - end - - context 'with a knative ingress gateway IP' do - let!(:serverless_domain_cluster) do - create( - :serverless_domain_cluster, - uuid: 'abcdef12345678', - pages_domain: pages_domain, - knative: knative_with_ingress - ) - end - - let(:serverless_domain) do - create( - :serverless_domain, - serverless_domain_cluster: serverless_domain_cluster, - environment: environment - ) - end - - it 'responds with 204 because of feature deprecation' do - query_host(serverless_domain.uri.host) + context 'when querying a custom domain' do + let_it_be(:pages_domain) { create(:pages_domain, domain: 'pages.io', project: project) } - expect(response).to have_gitlab_http_status(:no_content) - expect(response.body).to be_empty - - ## - # Serverless serving and reverse proxy to Kubernetes / Knative has - # been deprecated and disabled, as per - # https://gitlab.com/gitlab-org/gitlab-pages/-/issues/467 - # - # expect(response).to match_response_schema('internal/serverless/virtual_domain') - # expect(json_response['certificate']).to eq(pages_domain.certificate) - # expect(json_response['key']).to eq(pages_domain.key) - # - # expect(json_response['lookup_paths']).to eq( - # [ - # { - # 'source' => { - # 'type' => 'serverless', - # 'service' => "test-function.#{project.name}-#{project.id}-#{environment.slug}.#{serverless_domain_cluster.knative.hostname}", - # 'cluster' => { - # 'hostname' => serverless_domain_cluster.knative.hostname, - # 'address' => serverless_domain_cluster.knative.external_ip, - # 'port' => 443, - # 'cert' => serverless_domain_cluster.certificate, - # 'key' => serverless_domain_cluster.key - # } - # } - # } - # ] - # ) + context 'when there are no pages deployed for the related project' do + before do + project.mark_pages_as_not_deployed end - end - end - context 'custom domain' do - let(:namespace) { create(:namespace, name: 'gitlab-org') } - let(:project) { create(:project, namespace: namespace, name: 'gitlab-ce') } - let!(:pages_domain) { create(:pages_domain, domain: 'pages.io', project: project) } - - context 'when there are no pages deployed for the related project' do it 'responds with 204 No Content' do - query_host('pages.io') + get api('/internal/pages'), headers: auth_header, params: { host: 'pages.io' } expect(response).to have_gitlab_http_status(:no_content) end end context 'when there are pages deployed for the related project' do - it 'domain lookup is case insensitive' do - deploy_pages(project) + before do + project.mark_pages_as_deployed + end - query_host('Pages.IO') + it 'domain lookup is case insensitive' do + get api('/internal/pages'), headers: auth_header, params: { host: 'Pages.IO' } expect(response).to have_gitlab_http_status(:ok) end it 'responds with the correct domain configuration' do - deploy_pages(project) - - query_host('pages.io') + get api('/internal/pages'), headers: auth_header, params: { host: 'pages.io' } expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('internal/pages/virtual_domain') @@ -212,7 +116,9 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do 'sha256' => deployment.file_sha256, 'file_size' => deployment.size, 'file_count' => deployment.file_count - } + }, + 'unique_host' => nil, + 'root_directory' => deployment.root_directory } ] ) @@ -220,20 +126,67 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do end end - context 'namespaced domain' do - let(:group) { create(:group, name: 'mygroup') } + context 'when querying a unique domain' do + before_all do + project.project_setting.update!( + pages_unique_domain: 'unique-domain', + pages_unique_domain_enabled: true + ) + end - before do - allow(Settings.pages).to receive(:host).and_return('gitlab-pages.io') - allow(Gitlab.config.pages).to receive(:url).and_return("http://gitlab-pages.io") + context 'when there are no pages deployed for the related project' do + before do + project.mark_pages_as_not_deployed + end + + it 'responds with 204 No Content' do + get api('/internal/pages'), headers: auth_header, params: { host: 'unique-domain.example.com' } + + expect(response).to have_gitlab_http_status(:no_content) + end end - context 'regular project' do - it 'responds with the correct domain configuration' do - project = create(:project, group: group, name: 'myproject') - deploy_pages(project) + context 'when there are pages deployed for the related project' do + before do + project.mark_pages_as_deployed + end + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(pages_unique_domain: false) + end - query_host('mygroup.gitlab-pages.io') + context 'when there are no pages deployed for the related project' do + it 'responds with 204 No Content' do + get api('/internal/pages'), headers: auth_header, params: { host: 'unique-domain.example.com' } + + expect(response).to have_gitlab_http_status(:no_content) + end + end + end + + context 'when the unique domain is disabled' do + before do + project.project_setting.update!(pages_unique_domain_enabled: false) + end + + context 'when there are no pages deployed for the related project' do + it 'responds with 204 No Content' do + get api('/internal/pages'), headers: auth_header, params: { host: 'unique-domain.example.com' } + + expect(response).to have_gitlab_http_status(:no_content) + end + end + end + + it 'domain lookup is case insensitive' do + get api('/internal/pages'), headers: auth_header, params: { host: 'Unique-Domain.example.com' } + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'responds with the correct domain configuration' do + get api('/internal/pages'), headers: auth_header, params: { host: 'unique-domain.example.com' } expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('internal/pages/virtual_domain') @@ -245,7 +198,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do 'project_id' => project.id, 'access_control' => false, 'https_only' => false, - 'prefix' => '/myproject/', + 'prefix' => '/', 'source' => { 'type' => 'zip', 'path' => deployment.file.url(expire_at: 1.day.from_now), @@ -253,56 +206,119 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do 'sha256' => deployment.file_sha256, 'file_size' => deployment.size, 'file_count' => deployment.file_count - } + }, + 'unique_host' => 'unique-domain.example.com', + 'root_directory' => 'public' } ] ) end end + end - it 'avoids N+1 queries' do - project = create(:project, group: group) - deploy_pages(project) - - control = ActiveRecord::QueryRecorder.new { query_host('mygroup.gitlab-pages.io') } + context 'when querying a namespaced domain' do + before do + allow(Settings.pages).to receive(:host).and_return('gitlab-pages.io') + allow(Gitlab.config.pages).to receive(:url).and_return("http://gitlab-pages.io") + end - 3.times do - project = create(:project, group: group) - deploy_pages(project) + context 'when there are no pages deployed for the related project' do + before do + project.mark_pages_as_not_deployed end - expect { query_host('mygroup.gitlab-pages.io') }.not_to exceed_query_limit(control) + it 'responds with 204 No Content' do + get api('/internal/pages'), headers: auth_header, params: { host: "#{group.path}.gitlab-pages.io" } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('internal/pages/virtual_domain') + expect(json_response['lookup_paths']).to eq([]) + end end - context 'group root project' do - it 'responds with the correct domain configuration' do - project = create(:project, group: group, name: 'mygroup.gitlab-pages.io') - deploy_pages(project) + context 'when there are pages deployed for the related project' do + before do + project.mark_pages_as_deployed + end - query_host('mygroup.gitlab-pages.io') + context 'with a regular project' do + it 'responds with the correct domain configuration' do + get api('/internal/pages'), headers: auth_header, params: { host: "#{group.path}.gitlab-pages.io" } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('internal/pages/virtual_domain') + + deployment = project.pages_metadatum.pages_deployment + expect(json_response['lookup_paths']).to eq( + [ + { + 'project_id' => project.id, + 'access_control' => false, + 'https_only' => false, + 'prefix' => "/#{project.path}/", + 'source' => { + 'type' => 'zip', + 'path' => deployment.file.url(expire_at: 1.day.from_now), + 'global_id' => "gid://gitlab/PagesDeployment/#{deployment.id}", + 'sha256' => deployment.file_sha256, + 'file_size' => deployment.size, + 'file_count' => deployment.file_count + }, + 'unique_host' => nil, + 'root_directory' => 'public' + } + ] + ) + end + end - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('internal/pages/virtual_domain') + it 'avoids N+1 queries' do + control = ActiveRecord::QueryRecorder.new do + get api('/internal/pages'), headers: auth_header, params: { host: "#{group.path}.gitlab-pages.io" } + end - deployment = project.pages_metadatum.pages_deployment - expect(json_response['lookup_paths']).to eq( - [ - { - 'project_id' => project.id, - 'access_control' => false, - 'https_only' => false, - 'prefix' => '/', - 'source' => { - 'type' => 'zip', - 'path' => deployment.file.url(expire_at: 1.day.from_now), - 'global_id' => "gid://gitlab/PagesDeployment/#{deployment.id}", - 'sha256' => deployment.file_sha256, - 'file_size' => deployment.size, - 'file_count' => deployment.file_count + 3.times do + project = create(:project, group: group) + project.mark_pages_as_deployed + end + + expect { get api('/internal/pages'), headers: auth_header, params: { host: "#{group.path}.gitlab-pages.io" } } + .not_to exceed_query_limit(control) + end + + context 'with a group root project' do + before do + project.update!(path: "#{group.path}.gitlab-pages.io") + end + + it 'responds with the correct domain configuration' do + get api('/internal/pages'), headers: auth_header, params: { host: "#{group.path}.gitlab-pages.io" } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('internal/pages/virtual_domain') + + deployment = project.pages_metadatum.pages_deployment + expect(json_response['lookup_paths']).to eq( + [ + { + 'project_id' => project.id, + 'access_control' => false, + 'https_only' => false, + 'prefix' => '/', + 'source' => { + 'type' => 'zip', + 'path' => deployment.file.url(expire_at: 1.day.from_now), + 'global_id' => "gid://gitlab/PagesDeployment/#{deployment.id}", + 'sha256' => deployment.file_sha256, + 'file_size' => deployment.size, + 'file_count' => deployment.file_count + }, + 'unique_host' => nil, + 'root_directory' => 'public' } - } - ] - ) + ] + ) + end end end end diff --git a/spec/requests/api/internal/workhorse_spec.rb b/spec/requests/api/internal/workhorse_spec.rb index 99d0ecabbb7..2657abffae6 100644 --- a/spec/requests/api/internal/workhorse_spec.rb +++ b/spec/requests/api/internal/workhorse_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Internal::Workhorse, :allow_forgery_protection, feature_category: :not_owned do +RSpec.describe API::Internal::Workhorse, :allow_forgery_protection, feature_category: :shared do include WorkhorseHelpers context '/authorize_upload' do diff --git a/spec/requests/api/issue_links_spec.rb b/spec/requests/api/issue_links_spec.rb index 40d8f6d2395..fcb199a91a4 100644 --- a/spec/requests/api/issue_links_spec.rb +++ b/spec/requests/api/issue_links_spec.rb @@ -87,7 +87,7 @@ RSpec.describe API::IssueLinks, feature_category: :team_planning do end context 'when user does not have write access to given issue' do - it 'returns 404' do + it 'returns 403' do unauthorized_project = create(:project) target_issue = create(:issue, project: unauthorized_project) unauthorized_project.add_guest(user) @@ -95,8 +95,8 @@ RSpec.describe API::IssueLinks, feature_category: :team_planning do post api("/projects/#{project.id}/issues/#{issue.iid}/links", user), params: { target_project_id: unauthorized_project.id, target_issue_iid: target_issue.iid } - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq('No matching issue found. Make sure that you are adding a valid issue URL.') + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['message']).to eq("Couldn't link issue. You must have at least the Reporter role in both projects.") end end diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb index 0641c2135c1..eaa3c46d0ca 100644 --- a/spec/requests/api/issues/get_group_issues_spec.rb +++ b/spec/requests/api/issues/get_group_issues_spec.rb @@ -74,7 +74,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do let(:base_url) { "/groups/#{group.id}/issues" } shared_examples 'group issues statistics' do - it 'returns issues statistics' do + it 'returns issues statistics', :aggregate_failures do get api("/groups/#{group.id}/issues_statistics", user), params: params expect(response).to have_gitlab_http_status(:ok) @@ -346,7 +346,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do group_project.add_reporter(user) end - it 'exposes known attributes' do + it 'exposes known attributes', :aggregate_failures do get api(base_url, admin) expect(response).to have_gitlab_http_status(:ok) @@ -355,7 +355,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end it 'returns all group issues (including opened and closed)' do - get api(base_url, admin) + get api(base_url, admin, admin_mode: true) expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id]) end @@ -385,7 +385,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end it 'returns group confidential issues for admin' do - get api(base_url, admin), params: { state: :opened } + get api(base_url, admin, admin_mode: true), params: { state: :opened } expect_paginated_array_response([group_confidential_issue.id, group_issue.id]) end @@ -403,7 +403,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end context 'labels parameter' do - it 'returns an array of labeled group issues' do + it 'returns an array of labeled group issues', :aggregate_failures do get api(base_url, user), params: { labels: group_label.title } expect_paginated_array_response(group_issue.id) @@ -486,7 +486,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end end - it 'returns an array of issues found by iids' do + it 'returns an array of issues found by iids', :aggregate_failures do get api(base_url, user), params: { iids: [group_issue.iid] } expect_paginated_array_response(group_issue.id) @@ -505,14 +505,14 @@ RSpec.describe API::Issues, feature_category: :team_planning do expect_paginated_array_response([]) end - it 'returns an array of group issues with any label' do + it 'returns an array of group issues with any label', :aggregate_failures do get api(base_url, user), params: { labels: IssuableFinder::Params::FILTER_ANY } expect_paginated_array_response(group_issue.id) expect(json_response.first['id']).to eq(group_issue.id) end - it 'returns an array of group issues with any label with labels param as array' do + it 'returns an array of group issues with any label with labels param as array', :aggregate_failures do get api(base_url, user), params: { labels: [IssuableFinder::Params::FILTER_ANY] } expect_paginated_array_response(group_issue.id) @@ -555,7 +555,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do expect_paginated_array_response(group_closed_issue.id) end - it 'returns an array of issues with no milestone' do + it 'returns an array of issues with no milestone', :aggregate_failures do get api(base_url, user), params: { milestone: no_milestone_title } expect(response).to have_gitlab_http_status(:ok) @@ -688,28 +688,28 @@ RSpec.describe API::Issues, feature_category: :team_planning do let!(:issue2) { create(:issue, author: user2, project: group_project, created_at: 2.days.ago) } let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: group_project, created_at: 1.day.ago) } - it 'returns issues with by assignee_username' do + it 'returns issues with by assignee_username', :aggregate_failures do get api(base_url, user), params: { assignee_username: [assignee.username], scope: 'all' } expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id]) expect_paginated_array_response([issue3.id, group_confidential_issue.id]) end - it 'returns issues by assignee_username as string' do + it 'returns issues by assignee_username as string', :aggregate_failures do get api(base_url, user), params: { assignee_username: assignee.username, scope: 'all' } expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id]) expect_paginated_array_response([issue3.id, group_confidential_issue.id]) end - it 'returns error when multiple assignees are passed' do + it 'returns error when multiple assignees are passed', :aggregate_failures do get api(base_url, user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response["error"]).to include("allows one value, but found 2") end - it 'returns error when assignee_username and assignee_id are passed together' do + it 'returns error when assignee_username and assignee_id are passed together', :aggregate_failures do get api(base_url, user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' } expect(response).to have_gitlab_http_status(:bad_request) @@ -719,7 +719,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end describe "#to_reference" do - it 'exposes reference path in context of group' do + it 'exposes reference path in context of group', :aggregate_failures do get api(base_url, user) expect(json_response.first['references']['short']).to eq("##{group_closed_issue.iid}") @@ -735,7 +735,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do group_closed_issue.reload end - it 'exposes reference path in context of parent group' do + it 'exposes reference path in context of parent group', :aggregate_failures do get api("/groups/#{parent_group.id}/issues") expect(json_response.first['references']['short']).to eq("##{group_closed_issue.iid}") diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb index 6fc3903103b..137fba66eaa 100644 --- a/spec/requests/api/issues/get_project_issues_spec.rb +++ b/spec/requests/api/issues/get_project_issues_spec.rb @@ -99,7 +99,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end shared_examples 'project issues statistics' do - it 'returns project issues statistics' do + it 'returns project issues statistics', :aggregate_failures do get api("/projects/#{project.id}/issues_statistics", current_user), params: params expect(response).to have_gitlab_http_status(:ok) @@ -317,7 +317,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end it 'returns project confidential issues for admin' do - get api("#{base_url}/issues", admin) + get api("#{base_url}/issues", admin, admin_mode: true) expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) end @@ -526,7 +526,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id]) end - it 'exposes known attributes' do + it 'exposes known attributes', :aggregate_failures do get api("#{base_url}/issues", user) expect(response).to have_gitlab_http_status(:ok) @@ -607,28 +607,28 @@ RSpec.describe API::Issues, feature_category: :team_planning do let!(:issue2) { create(:issue, author: user2, project: project, created_at: 2.days.ago) } let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: project, created_at: 1.day.ago) } - it 'returns issues by assignee_username' do + it 'returns issues by assignee_username', :aggregate_failures do get api("/issues", user), params: { assignee_username: [assignee.username], scope: 'all' } expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id]) expect_paginated_array_response([confidential_issue.id, issue3.id]) end - it 'returns issues by assignee_username as string' do + it 'returns issues by assignee_username as string', :aggregate_failures do get api("/issues", user), params: { assignee_username: assignee.username, scope: 'all' } expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id]) expect_paginated_array_response([confidential_issue.id, issue3.id]) end - it 'returns error when multiple assignees are passed' do + it 'returns error when multiple assignees are passed', :aggregate_failures do get api("/issues", user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response["error"]).to include("allows one value, but found 2") end - it 'returns error when assignee_username and assignee_id are passed together' do + it 'returns error when assignee_username and assignee_id are passed together', :aggregate_failures do get api("/issues", user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' } expect(response).to have_gitlab_http_status(:bad_request) @@ -638,6 +638,12 @@ RSpec.describe API::Issues, feature_category: :team_planning do end describe 'GET /projects/:id/issues/:issue_iid' do + let(:path) { "/projects/#{project.id}/issues/#{confidential_issue.iid}" } + + it_behaves_like 'GET request permissions for admin mode' do + let(:failed_status_code) { :not_found } + end + context 'when unauthenticated' do it 'returns public issues' do get api("/projects/#{project.id}/issues/#{issue.iid}") @@ -646,7 +652,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end end - it 'exposes known attributes' do + it 'exposes known attributes', :aggregate_failures do get api("/projects/#{project.id}/issues/#{issue.iid}", user) expect(response).to have_gitlab_http_status(:ok) @@ -686,7 +692,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end end - it 'exposes the closed_at attribute' do + it 'exposes the closed_at attribute', :aggregate_failures do get api("/projects/#{project.id}/issues/#{closed_issue.iid}", user) expect(response).to have_gitlab_http_status(:ok) @@ -694,7 +700,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end context 'links exposure' do - it 'exposes related resources full URIs' do + it 'exposes related resources full URIs', :aggregate_failures do get api("/projects/#{project.id}/issues/#{issue.iid}", user) links = json_response['_links'] @@ -706,7 +712,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end end - it 'returns a project issue by internal id' do + it 'returns a project issue by internal id', :aggregate_failures do get api("/projects/#{project.id}/issues/#{issue.iid}", user) expect(response).to have_gitlab_http_status(:ok) @@ -727,43 +733,43 @@ RSpec.describe API::Issues, feature_category: :team_planning do context 'confidential issues' do it 'returns 404 for non project members' do - get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member) + get api(path, non_member) expect(response).to have_gitlab_http_status(:not_found) end it 'returns 404 for project members with guest role' do - get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest) + get api(path, guest) expect(response).to have_gitlab_http_status(:not_found) end - it 'returns confidential issue for project members' do - get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user) + it 'returns confidential issue for project members', :aggregate_failures do + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response['title']).to eq(confidential_issue.title) expect(json_response['iid']).to eq(confidential_issue.iid) end - it 'returns confidential issue for author' do - get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author) + it 'returns confidential issue for author', :aggregate_failures do + get api(path, author) expect(response).to have_gitlab_http_status(:ok) expect(json_response['title']).to eq(confidential_issue.title) expect(json_response['iid']).to eq(confidential_issue.iid) end - it 'returns confidential issue for assignee' do - get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", assignee) + it 'returns confidential issue for assignee', :aggregate_failures do + get api(path, assignee) expect(response).to have_gitlab_http_status(:ok) expect(json_response['title']).to eq(confidential_issue.title) expect(json_response['iid']).to eq(confidential_issue.iid) end - it 'returns confidential issue for admin' do - get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin) + it 'returns confidential issue for admin', :aggregate_failures do + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['title']).to eq(confidential_issue.title) @@ -829,7 +835,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do let!(:related_mr) { create_referencing_mr(user, project, issue) } context 'when unauthenticated' do - it 'return list of referenced merge requests from issue' do + it 'return list of referenced merge requests from issue', :aggregate_failures do get_related_merge_requests(project.id, issue.iid) expect_paginated_array_response(related_mr.id) @@ -890,6 +896,10 @@ RSpec.describe API::Issues, feature_category: :team_planning do describe 'GET /projects/:id/issues/:issue_iid/user_agent_detail' do let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) } + it_behaves_like 'GET request permissions for admin mode' do + let(:path) { "/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail" } + end + context 'when unauthenticated' do it 'returns unauthorized' do get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail") @@ -898,8 +908,8 @@ RSpec.describe API::Issues, feature_category: :team_planning do end end - it 'exposes known attributes' do - get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin) + it 'exposes known attributes', :aggregate_failures do + get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['user_agent']).to eq(user_agent_detail.user_agent) @@ -936,7 +946,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do ) end - it 'returns a full list of participants' do + it 'returns a full list of participants', :aggregate_failures do get api("/projects/#{project.id}/issues/#{issue.iid}/participants", user) expect(response).to have_gitlab_http_status(:ok) @@ -945,7 +955,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end context 'when user cannot see a confidential note' do - it 'returns a limited list of participants' do + it 'returns a limited list of participants', :aggregate_failures do get api("/projects/#{project.id}/issues/#{issue.iid}/participants", create(:user)) expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb index 4b60eaadcbc..af289352778 100644 --- a/spec/requests/api/issues/issues_spec.rb +++ b/spec/requests/api/issues/issues_spec.rb @@ -78,7 +78,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end shared_examples 'issues statistics' do - it 'returns issues statistics' do + it 'returns issues statistics', :aggregate_failures do get api("/issues_statistics", user), params: params expect(response).to have_gitlab_http_status(:ok) @@ -90,9 +90,13 @@ RSpec.describe API::Issues, feature_category: :team_planning do end describe 'GET /issues/:id' do + let(:path) { "/issues/#{issue.id}" } + + it_behaves_like 'GET request permissions for admin mode' + context 'when unauthorized' do it 'returns unauthorized' do - get api("/issues/#{issue.id}") + get api(path) expect(response).to have_gitlab_http_status(:unauthorized) end @@ -101,7 +105,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do context 'when authorized' do context 'as a normal user' do it 'returns forbidden' do - get api("/issues/#{issue.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:forbidden) end @@ -109,8 +113,8 @@ RSpec.describe API::Issues, feature_category: :team_planning do context 'as an admin' do context 'when issue exists' do - it 'returns the issue' do - get api("/issues/#{issue.id}", admin) + it 'returns the issue', :aggregate_failures do + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response.dig('author', 'id')).to eq(issue.author.id) @@ -121,7 +125,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do context 'when issue does not exist' do it 'returns 404' do - get api("/issues/0", admin) + get api("/issues/#{non_existing_record_id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end @@ -132,7 +136,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do describe 'GET /issues' do context 'when unauthenticated' do - it 'returns an array of all issues' do + it 'returns an array of all issues', :aggregate_failures do get api('/issues'), params: { scope: 'all' } expect(response).to have_gitlab_http_status(:ok) @@ -162,14 +166,14 @@ RSpec.describe API::Issues, feature_category: :team_planning do expect(response).to have_gitlab_http_status(:unauthorized) end - it 'returns an array of issues matching state in milestone' do + it 'returns an array of issues matching state in milestone', :aggregate_failures do get api('/issues'), params: { milestone: 'foo', scope: 'all' } expect(response).to have_gitlab_http_status(:ok) expect_paginated_array_response([]) end - it 'returns an array of issues matching state in milestone' do + it 'returns an array of issues matching state in milestone', :aggregate_failures do get api('/issues'), params: { milestone: milestone.title, scope: 'all' } expect(response).to have_gitlab_http_status(:ok) @@ -273,7 +277,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end context 'when authenticated' do - it 'returns an array of issues' do + it 'returns an array of issues', :aggregate_failures do get api('/issues', user) expect_paginated_array_response([issue.id, closed_issue.id]) @@ -532,7 +536,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do context 'with incident issues' do let_it_be(:incident) { create(:incident, project: project) } - it 'avoids N+1 queries' do + it 'avoids N+1 queries', :aggregate_failures do get api('/issues', user) # warm up control = ActiveRecord::QueryRecorder.new do @@ -553,7 +557,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do context 'with issues closed as duplicates' do let_it_be(:dup_issue_1) { create(:issue, :closed_as_duplicate, project: project) } - it 'avoids N+1 queries' do + it 'avoids N+1 queries', :aggregate_failures do get api('/issues', user) # warm up control = ActiveRecord::QueryRecorder.new do @@ -639,7 +643,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do expect_paginated_array_response([]) end - it 'returns an array of labeled issues matching given state' do + it 'returns an array of labeled issues matching given state', :aggregate_failures do get api('/issues', user), params: { labels: label.title, state: :opened } expect_paginated_array_response(issue.id) @@ -647,7 +651,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do expect(json_response.first['state']).to eq('opened') end - it 'returns an array of labeled issues matching given state with labels param as array' do + it 'returns an array of labeled issues matching given state with labels param as array', :aggregate_failures do get api('/issues', user), params: { labels: [label.title], state: :opened } expect_paginated_array_response(issue.id) @@ -917,14 +921,14 @@ RSpec.describe API::Issues, feature_category: :team_planning do end end - it 'matches V4 response schema' do + it 'matches V4 response schema', :aggregate_failures do get api('/issues', user) expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('public_api/v4/issues') end - it 'returns a related merge request count of 0 if there are no related merge requests' do + it 'returns a related merge request count of 0 if there are no related merge requests', :aggregate_failures do get api('/issues', user) expect(response).to have_gitlab_http_status(:ok) @@ -932,7 +936,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do expect(json_response.first).to include('merge_requests_count' => 0) end - it 'returns a related merge request count > 0 if there are related merge requests' do + it 'returns a related merge request count > 0 if there are related merge requests', :aggregate_failures do create(:merge_requests_closing_issues, issue: issue) get api('/issues', user) @@ -1013,28 +1017,28 @@ RSpec.describe API::Issues, feature_category: :team_planning do let!(:issue2) { create(:issue, author: user2, project: project, created_at: 2.days.ago) } let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: project, created_at: 1.day.ago) } - it 'returns issues with by assignee_username' do + it 'returns issues with by assignee_username', :aggregate_failures do get api("/issues", user), params: { assignee_username: [assignee.username], scope: 'all' } expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id]) expect_paginated_array_response([confidential_issue.id, issue3.id]) end - it 'returns issues by assignee_username as string' do + it 'returns issues by assignee_username as string', :aggregate_failures do get api("/issues", user), params: { assignee_username: assignee.username, scope: 'all' } expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id]) expect_paginated_array_response([confidential_issue.id, issue3.id]) end - it 'returns error when multiple assignees are passed' do + it 'returns error when multiple assignees are passed', :aggregate_failures do get api("/issues", user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response["error"]).to include("allows one value, but found 2") end - it 'returns error when assignee_username and assignee_id are passed together' do + it 'returns error when assignee_username and assignee_id are passed together', :aggregate_failures do get api("/issues", user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' } expect(response).to have_gitlab_http_status(:bad_request) @@ -1088,7 +1092,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end describe 'GET /projects/:id/issues/:issue_iid' do - it 'exposes full reference path' do + it 'exposes full reference path', :aggregate_failures do get api("/projects/#{project.id}/issues/#{issue.iid}", user) expect(response).to have_gitlab_http_status(:ok) @@ -1106,7 +1110,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end context 'user does not have permission to view new issue' do - it 'does not return the issue as closed_as_duplicate_of' do + it 'does not return the issue as closed_as_duplicate_of', :aggregate_failures do get api("/projects/#{project.id}/issues/#{issue_closed_as_dup.iid}", user) expect(response).to have_gitlab_http_status(:ok) @@ -1119,7 +1123,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do new_issue.project.add_guest(user) end - it 'returns the issue as closed_as_duplicate_of' do + it 'returns the issue as closed_as_duplicate_of', :aggregate_failures do get api("/projects/#{project.id}/issues/#{issue_closed_as_dup.iid}", user) expect(response).to have_gitlab_http_status(:ok) @@ -1131,7 +1135,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end describe "POST /projects/:id/issues" do - it 'creates a new project issue' do + it 'creates a new project issue', :aggregate_failures do post api("/projects/#{project.id}/issues", user), params: { title: 'new issue' } expect(response).to have_gitlab_http_status(:created) @@ -1139,6 +1143,15 @@ RSpec.describe API::Issues, feature_category: :team_planning do expect(json_response['issue_type']).to eq('issue') end + context 'when confidential is null' do + it 'responds with 400 error', :aggregate_failures do + post api("/projects/#{project.id}/issues", user), params: { title: 'issue', confidential: nil } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('confidential is empty') + end + end + context 'when issue create service returns an unrecoverable error' do before do allow_next_instance_of(Issues::CreateService) do |create_service| @@ -1146,7 +1159,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end end - it 'returns and error message and status code from the service' do + it 'returns and error message and status code from the service', :aggregate_failures do post api("/projects/#{project.id}/issues", user), params: { title: 'new issue' } expect(response).to have_gitlab_http_status(:forbidden) @@ -1160,6 +1173,11 @@ RSpec.describe API::Issues, feature_category: :team_planning do let(:entity) { issue } end + it_behaves_like 'PUT request permissions for admin mode' do + let(:path) { "/projects/#{project.id}/issues/#{issue.iid}" } + let(:params) { { labels: 'label1', updated_at: Time.new(2000, 1, 1) } } + end + describe 'updated_at param' do let(:fixed_time) { Time.new(2001, 1, 1) } let(:updated_at) { Time.new(2000, 1, 1) } @@ -1168,15 +1186,15 @@ RSpec.describe API::Issues, feature_category: :team_planning do travel_to fixed_time end - it 'allows admins to set the timestamp' do - put api("/projects/#{project.id}/issues/#{issue.iid}", admin), params: { labels: 'label1', updated_at: updated_at } + it 'allows admins to set the timestamp', :aggregate_failures do + put api("/projects/#{project.id}/issues/#{issue.iid}", admin, admin_mode: true), params: { labels: 'label1', updated_at: updated_at } expect(response).to have_gitlab_http_status(:ok) expect(Time.parse(json_response['updated_at'])).to be_like_time(updated_at) expect(ResourceLabelEvent.last.created_at).to be_like_time(updated_at) end - it 'does not allow other users to set the timestamp' do + it 'does not allow other users to set the timestamp', :aggregate_failures do reporter = create(:user) project.add_developer(reporter) @@ -1192,7 +1210,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do it 'allows issue type to be converted' do put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { issue_type: 'incident' } - expect(issue.reload.incident?).to be(true) + expect(issue.reload.work_item_type.incident?).to be(true) end end end @@ -1259,7 +1277,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end context 'with valid params' do - it 'reorders issues and returns a successful 200 response' do + it 'reorders issues and returns a successful 200 response', :aggregate_failures do put api("/projects/#{project.id}/issues/#{issue1.iid}/reorder", user), params: { move_after_id: issue2.id, move_before_id: issue3.id } expect(response).to have_gitlab_http_status(:ok) @@ -1286,7 +1304,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do let(:other_project) { create(:project, group: group) } let(:other_issue) { create(:issue, project: other_project, relative_position: 80) } - it 'reorders issues and returns a successful 200 response' do + it 'reorders issues and returns a successful 200 response', :aggregate_failures do put api("/projects/#{other_project.id}/issues/#{other_issue.iid}/reorder", user), params: { move_after_id: issue2.id, move_before_id: issue3.id } expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb index 265091fa698..5a15a0b6dad 100644 --- a/spec/requests/api/issues/post_projects_issues_spec.rb +++ b/spec/requests/api/issues/post_projects_issues_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Issues, feature_category: :team_planning do +RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_planning do let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) do create(:project, :public, creator_id: user.id, namespace: user.namespace) @@ -123,7 +123,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do context 'an internal ID is provided' do context 'by an admin' do it 'sets the internal ID on the new issue' do - post api("/projects/#{project.id}/issues", admin), + post api("/projects/#{project.id}/issues", admin, admin_mode: true), params: { title: 'new issue', iid: 9001 } expect(response).to have_gitlab_http_status(:created) @@ -167,7 +167,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do context 'when an issue with the same IID exists on database' do it 'returns 409' do - post api("/projects/#{project.id}/issues", admin), + post api("/projects/#{project.id}/issues", admin, admin_mode: true), params: { title: 'new issue', iid: issue.iid } expect(response).to have_gitlab_http_status(:conflict) @@ -337,7 +337,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do context 'by an admin' do it 'sets the creation time on the new issue' do - post api("/projects/#{project.id}/issues", admin), params: params + post api("/projects/#{project.id}/issues", admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:created) expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) @@ -475,9 +475,15 @@ RSpec.describe API::Issues, feature_category: :team_planning do describe '/projects/:id/issues/:issue_iid/move' do let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace) } let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace) } + let(:path) { "/projects/#{project.id}/issues/#{issue.iid}/move" } + + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { { to_project_id: target_project2.id } } + let(:failed_status_code) { 400 } + end it 'moves an issue' do - post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), + post api(path, user), params: { to_project_id: target_project.id } expect(response).to have_gitlab_http_status(:created) @@ -486,7 +492,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do context 'when source and target projects are the same' do it 'returns 400 when trying to move an issue' do - post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), + post api(path, user), params: { to_project_id: project.id } expect(response).to have_gitlab_http_status(:bad_request) @@ -496,7 +502,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do context 'when the user does not have the permission to move issues' do it 'returns 400 when trying to move an issue' do - post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), + post api(path, user), params: { to_project_id: target_project2.id } expect(response).to have_gitlab_http_status(:bad_request) @@ -505,7 +511,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end it 'moves the issue to another namespace if I am admin' do - post api("/projects/#{project.id}/issues/#{issue.iid}/move", admin), + post api(path, admin, admin_mode: true), params: { to_project_id: target_project2.id } expect(response).to have_gitlab_http_status(:created) @@ -544,7 +550,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do context 'when target project does not exist' do it 'returns 404 when trying to move an issue' do - post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), + post api(path, user), params: { to_project_id: 0 } expect(response).to have_gitlab_http_status(:not_found) diff --git a/spec/requests/api/issues/put_projects_issues_spec.rb b/spec/requests/api/issues/put_projects_issues_spec.rb index f0d174c9e78..217788c519f 100644 --- a/spec/requests/api/issues/put_projects_issues_spec.rb +++ b/spec/requests/api/issues/put_projects_issues_spec.rb @@ -80,7 +80,12 @@ RSpec.describe API::Issues, feature_category: :team_planning do end describe 'PUT /projects/:id/issues/:issue_iid to update only title' do - it 'updates a project issue' do + it_behaves_like 'PUT request permissions for admin mode' do + let(:path) { "/projects/#{project.id}/issues/#{confidential_issue.iid}" } + let(:params) { { title: updated_title } } + end + + it 'updates a project issue', :aggregate_failures do put api_for_user, params: { title: updated_title } expect(response).to have_gitlab_http_status(:ok) @@ -88,7 +93,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end it 'returns 404 error if issue iid not found' do - put api("/projects/#{project.id}/issues/44444", user), params: { title: updated_title } + put api("/projects/#{project.id}/issues/#{non_existing_record_id}", user), params: { title: updated_title } expect(response).to have_gitlab_http_status(:not_found) end @@ -109,7 +114,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do expect(response).to have_gitlab_http_status(:ok) end - it 'allows special label names with labels param as array' do + it 'allows special label names with labels param as array', :aggregate_failures do put api_for_user, params: { title: updated_title, @@ -135,42 +140,42 @@ RSpec.describe API::Issues, feature_category: :team_planning do expect(response).to have_gitlab_http_status(:forbidden) end - it 'updates a confidential issue for project members' do + it 'updates a confidential issue for project members', :aggregate_failures do put api(confidential_issue_path, user), params: { title: updated_title } expect(response).to have_gitlab_http_status(:ok) expect(json_response['title']).to eq(updated_title) end - it 'updates a confidential issue for author' do + it 'updates a confidential issue for author', :aggregate_failures do put api(confidential_issue_path, author), params: { title: updated_title } expect(response).to have_gitlab_http_status(:ok) expect(json_response['title']).to eq(updated_title) end - it 'updates a confidential issue for admin' do - put api(confidential_issue_path, admin), params: { title: updated_title } + it 'updates a confidential issue for admin', :aggregate_failures do + put api(confidential_issue_path, admin, admin_mode: true), params: { title: updated_title } expect(response).to have_gitlab_http_status(:ok) expect(json_response['title']).to eq(updated_title) end - it 'sets an issue to confidential' do + it 'sets an issue to confidential', :aggregate_failures do put api_for_user, params: { confidential: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response['confidential']).to be_truthy end - it 'makes a confidential issue public' do + it 'makes a confidential issue public', :aggregate_failures do put api(confidential_issue_path, user), params: { confidential: false } expect(response).to have_gitlab_http_status(:ok) expect(json_response['confidential']).to be_falsy end - it 'does not update a confidential issue with wrong confidential flag' do + it 'does not update a confidential issue with wrong confidential flag', :aggregate_failures do put api(confidential_issue_path, user), params: { confidential: 'foo' } expect(response).to have_gitlab_http_status(:bad_request) @@ -209,7 +214,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do expect { update_issue }.not_to change { issue.reload.title } end - it 'returns correct status and message' do + it 'returns correct status and message', :aggregate_failures do update_issue expect(response).to have_gitlab_http_status(:bad_request) @@ -246,14 +251,14 @@ RSpec.describe API::Issues, feature_category: :team_planning do describe 'PUT /projects/:id/issues/:issue_iid to update assignee' do context 'support for deprecated assignee_id' do - it 'removes assignee' do + it 'removes assignee', :aggregate_failures do put api_for_user, params: { assignee_id: 0 } expect(response).to have_gitlab_http_status(:ok) expect(json_response['assignee']).to be_nil end - it 'updates an issue with new assignee' do + it 'updates an issue with new assignee', :aggregate_failures do put api_for_user, params: { assignee_id: user2.id } expect(response).to have_gitlab_http_status(:ok) @@ -261,21 +266,21 @@ RSpec.describe API::Issues, feature_category: :team_planning do end end - it 'removes assignee' do + it 'removes assignee', :aggregate_failures do put api_for_user, params: { assignee_ids: [0] } expect(response).to have_gitlab_http_status(:ok) expect(json_response['assignees']).to be_empty end - it 'updates an issue with new assignee' do + it 'updates an issue with new assignee', :aggregate_failures do put api_for_user, params: { assignee_ids: [user2.id] } expect(response).to have_gitlab_http_status(:ok) expect(json_response['assignees'].first['name']).to eq(user2.name) end - context 'single assignee restrictions' do + context 'single assignee restrictions', :aggregate_failures do it 'updates an issue with several assignees but only one has been applied' do put api_for_user, params: { assignee_ids: [user2.id, guest.id] } @@ -289,7 +294,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do let!(:label) { create(:label, title: 'dummy', project: project) } let!(:label_link) { create(:label_link, label: label, target: issue) } - it 'adds relevant labels' do + it 'adds relevant labels', :aggregate_failures do put api_for_user, params: { add_labels: '1, 2' } expect(response).to have_gitlab_http_status(:ok) @@ -300,14 +305,14 @@ RSpec.describe API::Issues, feature_category: :team_planning do let!(:label2) { create(:label, title: 'a-label', project: project) } let!(:label_link2) { create(:label_link, label: label2, target: issue) } - it 'removes relevant labels' do + it 'removes relevant labels', :aggregate_failures do put api_for_user, params: { remove_labels: label2.title } expect(response).to have_gitlab_http_status(:ok) expect(json_response['labels']).to eq([label.title]) end - it 'removes all labels' do + it 'removes all labels', :aggregate_failures do put api_for_user, params: { remove_labels: "#{label.title}, #{label2.title}" } expect(response).to have_gitlab_http_status(:ok) @@ -315,14 +320,14 @@ RSpec.describe API::Issues, feature_category: :team_planning do end end - it 'does not update labels if not present' do + it 'does not update labels if not present', :aggregate_failures do put api_for_user, params: { title: updated_title } expect(response).to have_gitlab_http_status(:ok) expect(json_response['labels']).to eq([label.title]) end - it 'removes all labels and touches the record' do + it 'removes all labels and touches the record', :aggregate_failures do travel_to(2.minutes.from_now) do put api_for_user, params: { labels: '' } end @@ -332,7 +337,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do expect(json_response['updated_at']).to be > Time.current end - it 'removes all labels and touches the record with labels param as array' do + it 'removes all labels and touches the record with labels param as array', :aggregate_failures do travel_to(2.minutes.from_now) do put api_for_user, params: { labels: [''] } end @@ -342,7 +347,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do expect(json_response['updated_at']).to be > Time.current end - it 'updates labels and touches the record' do + it 'updates labels and touches the record', :aggregate_failures do travel_to(2.minutes.from_now) do put api_for_user, params: { labels: 'foo,bar' } end @@ -352,7 +357,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do expect(json_response['updated_at']).to be > Time.current end - it 'updates labels and touches the record with labels param as array' do + it 'updates labels and touches the record with labels param as array', :aggregate_failures do travel_to(2.minutes.from_now) do put api_for_user, params: { labels: %w(foo bar) } end @@ -363,21 +368,21 @@ RSpec.describe API::Issues, feature_category: :team_planning do expect(json_response['updated_at']).to be > Time.current end - it 'allows special label names' do + it 'allows special label names', :aggregate_failures do put api_for_user, params: { labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' } expect(response).to have_gitlab_http_status(:ok) expect(json_response['labels']).to contain_exactly('label:foo', 'label-bar', 'label_bar', 'label/bar', 'label?bar', 'label&bar', '?', '&') end - it 'allows special label names with labels param as array' do + it 'allows special label names with labels param as array', :aggregate_failures do put api_for_user, params: { labels: ['label:foo', 'label-bar', 'label_bar', 'label/bar,label?bar,label&bar,?,&'] } expect(response).to have_gitlab_http_status(:ok) expect(json_response['labels']).to contain_exactly('label:foo', 'label-bar', 'label_bar', 'label/bar', 'label?bar', 'label&bar', '?', '&') end - it 'returns 400 if title is too long' do + it 'returns 400 if title is too long', :aggregate_failures do put api_for_user, params: { title: 'g' * 256 } expect(response).to have_gitlab_http_status(:bad_request) @@ -386,7 +391,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end describe 'PUT /projects/:id/issues/:issue_iid to update state and label' do - it 'updates a project issue' do + it 'updates a project issue', :aggregate_failures do put api_for_user, params: { labels: 'label2', state_event: 'close' } expect(response).to have_gitlab_http_status(:ok) @@ -394,7 +399,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do expect(json_response['state']).to eq 'closed' end - it 'reopens a project isssue' do + it 'reopens a project isssue', :aggregate_failures do put api(issue_path, user), params: { state_event: 'reopen' } expect(response).to have_gitlab_http_status(:ok) @@ -404,7 +409,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do describe 'PUT /projects/:id/issues/:issue_iid to update updated_at param' do context 'when reporter makes request' do - it 'accepts the update date to be set' do + it 'accepts the update date to be set', :aggregate_failures do update_time = 2.weeks.ago put api_for_user, params: { title: 'some new title', updated_at: update_time } @@ -436,7 +441,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do expect(response).to have_gitlab_http_status(:bad_request) end - it 'accepts the update date to be set' do + it 'accepts the update date to be set', :aggregate_failures do update_time = 2.weeks.ago put api_for_owner, params: { title: 'some new title', updated_at: update_time } @@ -448,7 +453,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do end describe 'PUT /projects/:id/issues/:issue_iid to update due date' do - it 'creates a new project issue' do + it 'creates a new project issue', :aggregate_failures do due_date = 2.weeks.from_now.strftime('%Y-%m-%d') put api_for_user, params: { due_date: due_date } diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb index d9a0f061156..3f600d24891 100644 --- a/spec/requests/api/keys_spec.rb +++ b/spec/requests/api/keys_spec.rb @@ -2,31 +2,35 @@ require 'spec_helper' -RSpec.describe API::Keys, feature_category: :authentication_and_authorization do +RSpec.describe API::Keys, :aggregate_failures, feature_category: :system_access do let_it_be(:user) { create(:user) } let_it_be(:admin) { create(:admin) } let_it_be(:email) { create(:email, user: user) } let_it_be(:key) { create(:rsa_key_4096, user: user, expires_at: 1.day.from_now) } let_it_be(:fingerprint_md5) { 'df:73:db:29:3c:a5:32:cf:09:17:7e:8e:9d:de:d7:f7' } + let_it_be(:path) { "/keys/#{key.id}" } describe 'GET /keys/:uid' do + it_behaves_like 'GET request permissions for admin mode' + context 'when unauthenticated' do it 'returns authentication error' do - get api("/keys/#{key.id}") + get api(path) expect(response).to have_gitlab_http_status(:unauthorized) end end context 'when authenticated' do it 'returns 404 for non-existing key' do - get api('/keys/0', admin) + get api('/keys/0', admin, admin_mode: true) + expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Not found') end it 'returns single ssh key with user information' do - get api("/keys/#{key.id}", admin) - expect(response).to have_gitlab_http_status(:ok) + get api(path, admin, admin_mode: true) + expect(json_response['title']).to eq(key.title) expect(Time.parse(json_response['expires_at'])).to be_like_time(key.expires_at) expect(json_response['user']['id']).to eq(user.id) @@ -34,7 +38,7 @@ RSpec.describe API::Keys, feature_category: :authentication_and_authorization do end it "does not include the user's `is_admin` flag" do - get api("/keys/#{key.id}", admin) + get api(path, admin, admin_mode: true) expect(json_response['user']['is_admin']).to be_nil end @@ -42,31 +46,28 @@ RSpec.describe API::Keys, feature_category: :authentication_and_authorization do end describe 'GET /keys?fingerprint=' do - it 'returns authentication error' do - get api("/keys?fingerprint=#{fingerprint_md5}") + let_it_be(:path) { "/keys?fingerprint=#{fingerprint_md5}" } - expect(response).to have_gitlab_http_status(:unauthorized) - end + it_behaves_like 'GET request permissions for admin mode' - it 'returns authentication error when authenticated as user' do - get api("/keys?fingerprint=#{fingerprint_md5}", user) + it 'returns authentication error' do + get api(path, admin_mode: true) - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:unauthorized) end context 'when authenticated as admin' do context 'MD5 fingerprint' do it 'returns 404 for non-existing SSH md5 fingerprint' do - get api("/keys?fingerprint=11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11", admin) + get api("/keys?fingerprint=11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Key Not Found') end it 'returns user if SSH md5 fingerprint found' do - get api("/keys?fingerprint=#{fingerprint_md5}", admin) + get api(path, admin, admin_mode: true) - expect(response).to have_gitlab_http_status(:ok) expect(json_response['title']).to eq(key.title) expect(json_response['user']['id']).to eq(user.id) expect(json_response['user']['username']).to eq(user.username) @@ -74,14 +75,14 @@ RSpec.describe API::Keys, feature_category: :authentication_and_authorization do context 'with FIPS mode', :fips_mode do it 'returns 404 for non-existing SSH md5 fingerprint' do - get api("/keys?fingerprint=11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11", admin) + get api("/keys?fingerprint=11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']).to eq('Failed to return the key') end it 'returns 404 for existing SSH md5 fingerprint' do - get api("/keys?fingerprint=#{fingerprint_md5}", admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']).to eq('Failed to return the key') @@ -90,14 +91,14 @@ RSpec.describe API::Keys, feature_category: :authentication_and_authorization do end it 'returns 404 for non-existing SSH sha256 fingerprint' do - get api("/keys?fingerprint=#{URI.encode_www_form_component("SHA256:nUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo1lCg")}", admin) + get api("/keys?fingerprint=#{URI.encode_www_form_component("SHA256:nUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo1lCg")}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Key Not Found') end it 'returns user if SSH sha256 fingerprint found' do - get api("/keys?fingerprint=#{URI.encode_www_form_component("SHA256:" + key.fingerprint_sha256)}", admin) + get api("/keys?fingerprint=#{URI.encode_www_form_component("SHA256:" + key.fingerprint_sha256)}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['title']).to eq(key.title) @@ -106,7 +107,7 @@ RSpec.describe API::Keys, feature_category: :authentication_and_authorization do end it 'returns user if SSH sha256 fingerprint found' do - get api("/keys?fingerprint=#{URI.encode_www_form_component("sha256:" + key.fingerprint_sha256)}", admin) + get api("/keys?fingerprint=#{URI.encode_www_form_component("sha256:" + key.fingerprint_sha256)}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['title']).to eq(key.title) @@ -115,7 +116,7 @@ RSpec.describe API::Keys, feature_category: :authentication_and_authorization do end it "does not include the user's `is_admin` flag" do - get api("/keys?fingerprint=#{URI.encode_www_form_component("sha256:" + key.fingerprint_sha256)}", admin) + get api("/keys?fingerprint=#{URI.encode_www_form_component("sha256:" + key.fingerprint_sha256)}", admin, admin_mode: true) expect(json_response['user']['is_admin']).to be_nil end @@ -136,7 +137,7 @@ RSpec.describe API::Keys, feature_category: :authentication_and_authorization do it 'returns user and projects if SSH sha256 fingerprint for DeployKey found' do user.keys << deploy_key - get api("/keys?fingerprint=#{URI.encode_www_form_component("SHA256:" + deploy_key.fingerprint_sha256)}", admin) + get api("/keys?fingerprint=#{URI.encode_www_form_component("SHA256:" + deploy_key.fingerprint_sha256)}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['title']).to eq(deploy_key.title) diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb index 82b87007a9b..05a9d98a9d0 100644 --- a/spec/requests/api/lint_spec.rb +++ b/spec/requests/api/lint_spec.rb @@ -2,224 +2,14 @@ require 'spec_helper' -RSpec.describe API::Lint, feature_category: :pipeline_authoring do +RSpec.describe API::Lint, feature_category: :pipeline_composition do describe 'POST /ci/lint' do - context 'when signup settings are disabled' do - before do - Gitlab::CurrentSettings.signup_enabled = false - end - - context 'when unauthenticated' do - it 'returns authentication error' do - post api('/ci/lint'), params: { content: 'content' } - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - - context 'when authenticated' do - let_it_be(:api_user) { create(:user) } - - it 'returns authorized' do - post api('/ci/lint', api_user), params: { content: 'content' } - - expect(response).to have_gitlab_http_status(:ok) - end - end - - context 'when authenticated as external user' do - let(:project) { create(:project) } - let(:api_user) { create(:user, :external) } - - context 'when reporter in a project' do - before do - project.add_reporter(api_user) - end - - it 'returns authorization failure' do - post api('/ci/lint', api_user), params: { content: 'content' } - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - - context 'when developer in a project' do - before do - project.add_developer(api_user) - end - - it 'returns authorization success' do - post api('/ci/lint', api_user), params: { content: 'content' } - - expect(response).to have_gitlab_http_status(:ok) - end - end - end - end + it 'responds with a 410' do + user = create(:user) - context 'when signup is enabled and not limited' do - before do - Gitlab::CurrentSettings.signup_enabled = true - stub_application_setting(domain_allowlist: [], email_restrictions_enabled: false, require_admin_approval_after_user_signup: false) - end - - context 'when unauthenticated' do - it 'returns authorized success' do - post api('/ci/lint'), params: { content: 'content' } - - expect(response).to have_gitlab_http_status(:ok) - end - end - - context 'when authenticated' do - let_it_be(:api_user) { create(:user) } - - it 'returns authentication success' do - post api('/ci/lint', api_user), params: { content: 'content' } - - expect(response).to have_gitlab_http_status(:ok) - end - end - end - - context 'when limited signup is enabled' do - before do - stub_application_setting(domain_allowlist: ['www.gitlab.com']) - Gitlab::CurrentSettings.signup_enabled = true - end - - context 'when unauthenticated' do - it 'returns unauthorized' do - post api('/ci/lint'), params: { content: 'content' } - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - - context 'when authenticated' do - let_it_be(:api_user) { create(:user) } - - it 'returns authentication success' do - post api('/ci/lint', api_user), params: { content: 'content' } - - expect(response).to have_gitlab_http_status(:ok) - end - end - end - - context 'when authenticated' do - let_it_be(:api_user) { create(:user) } + post api('/ci/lint', user), params: { content: "test_job:\n script: ls" } - context 'with valid .gitlab-ci.yml content' do - let(:yaml_content) do - File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) - end - - it 'passes validation without warnings or errors' do - post api('/ci/lint', api_user), params: { content: yaml_content } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_an Hash - expect(json_response['status']).to eq('valid') - expect(json_response['warnings']).to match_array([]) - expect(json_response['errors']).to match_array([]) - expect(json_response['includes']).to eq([]) - end - - it 'outputs expanded yaml content' do - post api('/ci/lint', api_user), params: { content: yaml_content, include_merged_yaml: true } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to have_key('merged_yaml') - end - - it 'outputs jobs' do - post api('/ci/lint', api_user), params: { content: yaml_content, include_jobs: true } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to have_key('jobs') - end - end - - context 'with valid .gitlab-ci.yml with warnings' do - let(:yaml_content) { { job: { script: 'ls', rules: [{ when: 'always' }] } }.to_yaml } - - it 'passes validation but returns warnings' do - post api('/ci/lint', api_user), params: { content: yaml_content } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['status']).to eq('valid') - expect(json_response['warnings']).not_to be_empty - expect(json_response['errors']).to match_array([]) - end - end - - context 'with an invalid .gitlab-ci.yml' do - context 'with invalid syntax' do - let(:yaml_content) { 'invalid content' } - - it 'responds with errors about invalid syntax' do - post api('/ci/lint', api_user), params: { content: yaml_content } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['status']).to eq('invalid') - expect(json_response['warnings']).to eq([]) - expect(json_response['errors']).to eq(['Invalid configuration format']) - expect(json_response['includes']).to eq(nil) - end - - it 'outputs expanded yaml content' do - post api('/ci/lint', api_user), params: { content: yaml_content, include_merged_yaml: true } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to have_key('merged_yaml') - end - - it 'outputs jobs' do - post api('/ci/lint', api_user), params: { content: yaml_content, include_jobs: true } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to have_key('jobs') - end - end - - context 'with invalid configuration' do - let(:yaml_content) { '{ image: "image:1.0", services: ["postgres"] }' } - - it 'responds with errors about invalid configuration' do - post api('/ci/lint', api_user), params: { content: yaml_content } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['status']).to eq('invalid') - expect(json_response['warnings']).to eq([]) - expect(json_response['errors']).to eq(['jobs config should contain at least one visible job']) - expect(json_response['includes']).to eq([]) - end - - it 'outputs expanded yaml content' do - post api('/ci/lint', api_user), params: { content: yaml_content, include_merged_yaml: true } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to have_key('merged_yaml') - end - - it 'outputs jobs' do - post api('/ci/lint', api_user), params: { content: yaml_content, include_jobs: true } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to have_key('jobs') - end - end - end - - context 'without the content parameter' do - it 'responds with validation error about missing content' do - post api('/ci/lint', api_user) - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']).to eq('content is missing') - end - end + expect(response).to have_gitlab_http_status(:gone) end end @@ -245,8 +35,8 @@ RSpec.describe API::Lint, feature_category: :pipeline_authoring do it 'passes validation' do ci_lint - included_config = YAML.safe_load(included_content, [Symbol]) - root_config = YAML.safe_load(yaml_content, [Symbol]) + included_config = YAML.safe_load(included_content, permitted_classes: [Symbol]) + root_config = YAML.safe_load(yaml_content, permitted_classes: [Symbol]) expected_yaml = included_config.merge(root_config).except(:include).deep_stringify_keys.to_yaml expect(response).to have_gitlab_http_status(:ok) @@ -535,8 +325,8 @@ RSpec.describe API::Lint, feature_category: :pipeline_authoring do it 'passes validation' do ci_lint - included_config = YAML.safe_load(included_content, [Symbol]) - root_config = YAML.safe_load(yaml_content, [Symbol]) + included_config = YAML.safe_load(included_content, permitted_classes: [Symbol]) + root_config = YAML.safe_load(yaml_content, permitted_classes: [Symbol]) expected_yaml = included_config.merge(root_config).except(:include).deep_stringify_keys.to_yaml expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb index 20aa660d95b..60e91973b5d 100644 --- a/spec/requests/api/maven_packages_spec.rb +++ b/spec/requests/api/maven_packages_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe API::MavenPackages, feature_category: :package_registry do using RSpec::Parameterized::TableSyntax include WorkhorseHelpers + include HttpBasicAuthHelpers include_context 'workhorse headers' @@ -22,7 +23,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do let_it_be(:deploy_token_for_group) { create(:deploy_token, :group, read_package_registry: true, write_package_registry: true) } let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token_for_group, group: group) } - let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_maven_user' } } + let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_maven_user' } } let(:package_name) { 'com/example/my-app' } let(:headers) { workhorse_headers } @@ -159,56 +160,149 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do end end - shared_examples 'downloads with a deploy token' do - context 'successful download' do + shared_examples 'allowing the download' do + it 'allows download' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + end + + shared_examples 'not allowing the download with' do |not_found_response| + it 'does not allow the download' do + subject + + expect(response).to have_gitlab_http_status(not_found_response) + end + end + + shared_examples 'downloads with a personal access token' do |not_found_response| + where(:valid, :sent_using) do + true | :custom_header + false | :custom_header + true | :basic_auth + false | :basic_auth + end + + with_them do + let(:token) { valid ? personal_access_token.token : 'not_valid' } + let(:headers) do + case sent_using + when :custom_header + { 'Private-Token' => token } + when :basic_auth + basic_auth_header(user.username, token) + end + end + subject do download_file( file_name: package_file.file_name, - request_headers: { Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token.token } + request_headers: headers ) end - it 'allows download with deploy token' do - subject + if params[:valid] + it_behaves_like 'allowing the download' + else + expected_status_code = not_found_response + # invalid PAT values sent through headers are validated. + # Invalid values will trigger an :unauthorized response (and not set current_user to nil) + expected_status_code = :unauthorized if params[:sent_using] == :custom_header && !params[:valid] + it_behaves_like 'not allowing the download with', expected_status_code + end + end + end - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq('application/octet-stream') + shared_examples 'downloads with a deploy token' do |not_found_response| + where(:valid, :sent_using) do + true | :custom_header + false | :custom_header + true | :basic_auth + false | :basic_auth + end + + with_them do + let(:token) { valid ? deploy_token.token : 'not_valid' } + let(:headers) do + case sent_using + when :custom_header + { Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => token } + when :basic_auth + basic_auth_header(deploy_token.username, token) + end end - it 'allows download with deploy token with only write_package_registry scope' do - deploy_token.update!(read_package_registry: false) + subject do + download_file( + file_name: package_file.file_name, + request_headers: headers + ) + end - subject + if params[:valid] + it_behaves_like 'allowing the download' - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq('application/octet-stream') + context 'with only write_package_registry scope' do + it_behaves_like 'allowing the download' do + before do + deploy_token.update!(read_package_registry: false) + end + end + end + else + it_behaves_like 'not allowing the download with', not_found_response end end end shared_examples 'downloads with a job token' do - context 'with a running job' do - it 'allows download with job token' do - download_file(file_name: package_file.file_name, params: { job_token: job.token }) + where(:valid, :sent_using) do + true | :custom_params + false | :custom_params + true | :basic_auth + false | :basic_auth + end - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq('application/octet-stream') + with_them do + let(:token) { valid ? job.token : 'not_valid' } + let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, token) } + let(:params) { { job_token: token } } + + subject do + case sent_using + when :custom_params + download_file(file_name: package_file.file_name, params: params) + when :basic_auth + download_file(file_name: package_file.file_name, request_headers: headers) + end end - end - context 'with a finished job' do - before do - job.update!(status: :failed) + context 'with a running job' do + if params[:valid] + it_behaves_like 'allowing the download' + else + it_behaves_like 'not allowing the download with', :unauthorized + end end - it 'returns unauthorized error' do - download_file(file_name: package_file.file_name, params: { job_token: job.token }) + context 'with a finished job' do + before do + job.update!(status: :failed) + end - expect(response).to have_gitlab_http_status(:unauthorized) + it_behaves_like 'not allowing the download with', :unauthorized end end end + shared_examples 'downloads with different tokens' do |not_found_response| + it_behaves_like 'downloads with a personal access token', not_found_response + it_behaves_like 'downloads with a deploy token', not_found_response + it_behaves_like 'downloads with a job token' + end + shared_examples 'successfully returning the file' do it 'returns the file', :aggregate_failures do subject @@ -285,6 +379,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do describe 'GET /api/v4/packages/maven/*path/:file_name' do context 'a public project' do + let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_maven_user' } } + subject { download_file(file_name: package_file.file_name) } shared_examples 'getting a file' do @@ -336,11 +432,10 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do it 'denies download when no private token' do download_file(file_name: package_file.file_name) - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:unauthorized) end - it_behaves_like 'downloads with a job token' - it_behaves_like 'downloads with a deploy token' + it_behaves_like 'downloads with different tokens', :unauthorized context 'with a non existing maven path' do subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') } @@ -377,11 +472,10 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do it 'denies download when no private token' do download_file(file_name: package_file.file_name) - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:unauthorized) end - it_behaves_like 'downloads with a job token' - it_behaves_like 'downloads with a deploy token' + it_behaves_like 'downloads with different tokens', :unauthorized it 'does not allow download by a unauthorized deploy token with same id as a user with access' do unauthorized_deploy_token = create(:deploy_token, read_package_registry: true, write_package_registry: true) @@ -411,12 +505,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do end context 'project name is different from a package name' do - before do - maven_metadatum.update!(path: "wrong_name/#{package.version}") - end - it 'rejects request' do - download_file(file_name: package_file.file_name) + download_file(file_name: package_file.file_name, path: "wrong_name/#{package.version}") expect(response).to have_gitlab_http_status(:forbidden) end @@ -451,6 +541,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do it_behaves_like 'forwarding package requests' context 'a public project' do + let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_maven_user' } } + subject { download_file(file_name: package_file.file_name) } shared_examples 'getting a file for a group' do @@ -496,8 +588,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do expect(response).to have_gitlab_http_status(not_found_response) end - it_behaves_like 'downloads with a job token' - it_behaves_like 'downloads with a deploy token' + it_behaves_like 'downloads with different tokens', not_found_response context 'with a non existing maven path' do subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') } @@ -506,7 +597,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do end end - it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { internal: :not_found, public: :redirect } + it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { internal: :unauthorized, public: :redirect } end context 'private project' do @@ -535,8 +626,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do expect(response).to have_gitlab_http_status(not_found_response) end - it_behaves_like 'downloads with a job token' - it_behaves_like 'downloads with a deploy token' + it_behaves_like 'downloads with different tokens', not_found_response context 'with a non existing maven path' do subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') } @@ -566,7 +656,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do end end - it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { private: :not_found, internal: :not_found, public: :redirect } + it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { private: :unauthorized, internal: :unauthorized, public: :redirect } context 'with a reporter from a subgroup accessing the root group' do let_it_be(:root_group) { create(:group, :private) } @@ -660,6 +750,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do describe 'GET /api/v4/projects/:id/packages/maven/*path/:file_name' do context 'a public project' do + let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_maven_user' } } + subject { download_file(file_name: package_file.file_name) } it_behaves_like 'tracking the file download event' @@ -718,7 +810,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do it 'denies download when no private token' do download_file(file_name: package_file.file_name) - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(:unauthorized) end context 'with access to package registry for everyone' do @@ -731,8 +823,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do it_behaves_like 'successfully returning the file' end - it_behaves_like 'downloads with a job token' - it_behaves_like 'downloads with a deploy token' + it_behaves_like 'downloads with different tokens', :unauthorized context 'with a non existing maven path' do subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') } @@ -901,8 +992,6 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do it_behaves_like 'package workhorse uploads' context 'event tracking' do - let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_maven_user' } } - it_behaves_like 'a package tracking event', described_class.name, 'push_package' context 'when the package file fails to be created' do @@ -917,6 +1006,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do end it 'creates package and stores package file' do + expect_use_primary + expect { upload_file_with_token(params: params) }.to change { project.packages.count }.by(1) .and change { Packages::Maven::Metadatum.count }.by(1) .and change { Packages::PackageFile.count }.by(1) @@ -962,6 +1053,17 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do expect(response).to have_gitlab_http_status(:forbidden) end + context 'file name is too long' do + let(:file_name) { 'a' * (Packages::Maven::FindOrCreatePackageService::MAX_FILE_NAME_LENGTH + 1) } + + it 'rejects request' do + expect { upload_file_with_token(params: params, file_name: file_name) }.not_to change { project.packages.count } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to include('File name is too long') + end + end + context 'version is not correct' do let(:version) { '$%123' } @@ -981,9 +1083,9 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do package_settings.update!(maven_duplicates_allowed: false) end - shared_examples 'storing the package file' do + shared_examples 'storing the package file' do |file_name: 'my-app-1.0-20180724.124855-1'| it 'stores the file', :aggregate_failures do - expect { upload_file_with_token(params: params) }.to change { package.package_files.count }.by(1) + expect { upload_file_with_token(params: params, file_name: file_name) }.to change { package.package_files.count }.by(1) expect(response).to have_gitlab_http_status(:ok) expect(jar_file.file_name).to eq(file_upload.original_filename) @@ -1023,6 +1125,10 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do it_behaves_like 'storing the package file' end + + context 'when uploading a similar package file name with a classifier' do + it_behaves_like 'storing the package file', file_name: 'my-app-1.0-20180724.124855-1-javadoc' + end end context 'for sha1 file' do @@ -1043,6 +1149,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do end it 'returns no content' do + expect_use_primary + upload expect(response).to have_gitlab_http_status(:no_content) @@ -1072,6 +1180,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do subject { upload_file_with_token(params: params, file_extension: 'jar.md5') } it 'returns an empty body' do + expect_use_primary + subject expect(response.body).to eq('') @@ -1086,10 +1196,40 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do end end end + + context 'reading fingerprints from UploadedFile instance' do + let(:file) { Packages::Package.last.package_files.with_format('%.jar').last } + + subject { upload_file_with_token(params: params) } + + before do + allow_next_instance_of(UploadedFile) do |uploaded_file| + allow(uploaded_file).to receive(:size).and_return(123) + allow(uploaded_file).to receive(:sha1).and_return('sha1') + allow(uploaded_file).to receive(:md5).and_return('md5') + end + end + + it 'reads size, sha1 and md5 fingerprints from uploaded_file instance' do + subject + + expect(file.size).to eq(123) + expect(file.file_sha1).to eq('sha1') + expect(file.file_md5).to eq('md5') + end + end + + def expect_use_primary + lb_session = ::Gitlab::Database::LoadBalancing::Session.current + + expect(lb_session).to receive(:use_primary).and_call_original + + allow(::Gitlab::Database::LoadBalancing::Session).to receive(:current).and_return(lb_session) + end end - def upload_file(params: {}, request_headers: headers, file_extension: 'jar') - url = "/projects/#{project.id}/packages/maven/#{param_path}/my-app-1.0-20180724.124855-1.#{file_extension}" + def upload_file(params: {}, request_headers: headers, file_extension: 'jar', file_name: 'my-app-1.0-20180724.124855-1') + url = "/projects/#{project.id}/packages/maven/#{param_path}/#{file_name}.#{file_extension}" workhorse_finalize( api(url), method: :put, @@ -1100,8 +1240,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do ) end - def upload_file_with_token(params: {}, request_headers: headers_with_token, file_extension: 'jar') - upload_file(params: params, request_headers: request_headers, file_extension: file_extension) + def upload_file_with_token(params: {}, request_headers: headers_with_token, file_extension: 'jar', file_name: 'my-app-1.0-20180724.124855-1') + upload_file(params: params, request_headers: request_headers, file_name: file_name, file_extension: file_extension) end end diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 4eff5e96e9c..353fddcb08d 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -130,6 +130,8 @@ RSpec.describe API::Members, feature_category: :subgroups do let(:project_user) { create(:user) } let(:linked_group_user) { create(:user) } let!(:project_group_link) { create(:project_group_link, project: project, group: linked_group) } + let(:invited_group_developer) { create(:user, username: 'invited_group_developer') } + let(:invited_group) { create(:group) { |group| group.add_developer(invited_group_developer) } } let(:project) do create(:project, :public, group: nested_group) do |project| @@ -146,19 +148,21 @@ RSpec.describe API::Members, feature_category: :subgroups do let(:nested_group) do create(:group, parent: group) do |nested_group| nested_group.add_developer(nested_user) + create(:group_group_link, :guest, shared_with_group: invited_group, shared_group: nested_group) end end - it 'finds all project members including inherited members' do + it 'finds all project members including inherited members and members shared into ancestor groups' do get api("/projects/#{project.id}/members/all", developer) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.map { |u| u['id'] }).to match_array [maintainer.id, developer.id, nested_user.id, project_user.id, linked_group_user.id] + expected_user_ids = [maintainer.id, developer.id, nested_user.id, project_user.id, linked_group_user.id, invited_group_developer.id] + expect(json_response.map { |u| u['id'] }).to match_array expected_user_ids end - it 'returns only one member for each user without returning duplicated members' do + it 'returns only one member for each user without returning duplicated members with correct access levels' do linked_group.add_developer(developer) get api("/projects/#{project.id}/members/all", developer) @@ -172,7 +176,8 @@ RSpec.describe API::Members, feature_category: :subgroups do [maintainer.id, Gitlab::Access::OWNER], [nested_user.id, Gitlab::Access::DEVELOPER], [project_user.id, Gitlab::Access::DEVELOPER], - [linked_group_user.id, Gitlab::Access::DEVELOPER] + [linked_group_user.id, Gitlab::Access::DEVELOPER], + [invited_group_developer.id, Gitlab::Access::GUEST] ] expect(json_response.map { |u| [u['id'], u['access_level']] }).to match_array(expected_users_and_access_levels) end @@ -183,7 +188,8 @@ RSpec.describe API::Members, feature_category: :subgroups do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.map { |u| u['id'] }).to match_array [maintainer.id, developer.id, nested_user.id] + expected_user_ids = [maintainer.id, developer.id, nested_user.id, invited_group_developer.id] + expect(json_response.map { |u| u['id'] }).to match_array expected_user_ids end context 'with a subgroup' do @@ -739,6 +745,30 @@ RSpec.describe API::Members, feature_category: :subgroups do end.to change { source.members.count }.by(-1) end + it_behaves_like 'rate limited endpoint', rate_limit_key: :member_delete do + let(:current_user) { maintainer } + + let(:another_member) { create(:user) } + + before do + source.add_developer(another_member) + end + + # We rate limit scoped by the group / project + let(:delete_paths) do + [ + api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", maintainer), + api("/#{source_type.pluralize}/#{source.id}/members/#{another_member.id}", maintainer) + ] + end + + def request + delete_member_path = delete_paths.shift + + delete delete_member_path + end + end + it_behaves_like '412 response' do let(:request) { api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", maintainer) } end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 19a630e5218..50e70a9dc0f 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -RSpec.describe API::MergeRequests, feature_category: :source_code_management do +RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :source_code_management do include ProjectForksHelper let_it_be(:base_time) { Time.now } @@ -50,6 +50,27 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do expect_successful_response_with_paginated_array end + context 'when merge request is unchecked' do + let(:check_service_class) { MergeRequests::MergeabilityCheckService } + let(:mr_entity) { json_response.find { |mr| mr['id'] == merge_request.id } } + let(:merge_request) { create(:merge_request, :simple, author: user, source_project: project, title: "Test") } + + before do + merge_request.mark_as_unchecked! + end + + context 'with merge status recheck projection' do + it 'does not enqueue a merge status recheck' do + expect(check_service_class).not_to receive(:new) + + get(api(endpoint_path), params: { with_merge_status_recheck: true }) + + expect_successful_response_with_paginated_array + expect(mr_entity['merge_status']).to eq('unchecked') + end + end + end + it_behaves_like 'issuable API rate-limited search' do let(:url) { endpoint_path } let(:issuable) { merge_request } @@ -85,28 +106,67 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do merge_request.mark_as_unchecked! end - context 'with merge status recheck projection' do - it 'checks mergeability asynchronously' do - expect_next_instances_of(check_service_class, (1..2)) do |service| - expect(service).not_to receive(:execute) - expect(service).to receive(:async_execute).and_call_original + context 'with a developer+ role' do + before do + project.add_developer(user2) + end + + context 'with merge status recheck projection' do + it 'checks mergeability asynchronously' do + expect_next_instances_of(check_service_class, (1..2)) do |service| + expect(service).not_to receive(:execute) + expect(service).to receive(:async_execute).and_call_original + end + + get(api(endpoint_path, user2), params: { with_merge_status_recheck: true }) + + expect_successful_response_with_paginated_array + expect(mr_entity['merge_status']).to eq('checking') end + end - get(api(endpoint_path, user), params: { with_merge_status_recheck: true }) + context 'without merge status recheck projection' do + it 'does not enqueue a merge status recheck' do + expect(check_service_class).not_to receive(:new) - expect_successful_response_with_paginated_array - expect(mr_entity['merge_status']).to eq('checking') + get api(endpoint_path, user2) + + expect_successful_response_with_paginated_array + expect(mr_entity['merge_status']).to eq('unchecked') + end end end - context 'without merge status recheck projection' do - it 'does not enqueue a merge status recheck' do - expect(check_service_class).not_to receive(:new) + context 'with a reporter role' do + context 'with merge status recheck projection' do + it 'does not enqueue a merge status recheck' do + expect(check_service_class).not_to receive(:new) - get api(endpoint_path, user) + get(api(endpoint_path, user2), params: { with_merge_status_recheck: true }) - expect_successful_response_with_paginated_array - expect(mr_entity['merge_status']).to eq('unchecked') + expect_successful_response_with_paginated_array + expect(mr_entity['merge_status']).to eq('unchecked') + end + end + + context 'when restrict_merge_status_recheck FF is disabled' do + before do + stub_feature_flags(restrict_merge_status_recheck: false) + end + + context 'with merge status recheck projection' do + it 'does enqueue a merge status recheck' do + expect_next_instances_of(check_service_class, (1..2)) do |service| + expect(service).not_to receive(:execute) + expect(service).to receive(:async_execute).and_call_original + end + + get(api(endpoint_path, user2), params: { with_merge_status_recheck: true }) + + expect_successful_response_with_paginated_array + expect(mr_entity['merge_status']).to eq('checking') + end + end end end end @@ -168,6 +228,17 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do end end + context 'when DB timeouts occur' do + it 'returns a :request_timeout status' do + allow(MergeRequestsFinder).to receive(:new).and_raise(ActiveRecord::QueryCanceled) + + path = endpoint_path + '?view=simple' + get api(path, user) + + expect(response).to have_gitlab_http_status(:request_timeout) + end + end + it 'returns an array of all merge_requests using simple mode' do path = endpoint_path + '?view=simple' @@ -238,6 +309,35 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do expect(response).to match_response_schema('public_api/v4/merge_requests') end + context 'with approved param' do + let(:approved_mr) { create(:merge_request, target_project: project, source_project: project) } + + before do + create(:approval, merge_request: approved_mr) + end + + it 'returns only approved merge requests' do + path = endpoint_path + '?approved=yes' + + get api(path, user) + + expect_paginated_array_response([approved_mr.id]) + end + + it 'returns only non-approved merge requests' do + path = endpoint_path + '?approved=no' + + get api(path, user) + + expect_paginated_array_response([ + merge_request_merged.id, + merge_request_locked.id, + merge_request_closed.id, + merge_request.id + ]) + end + end + it 'returns an empty array if no issue matches milestone' do get api(endpoint_path, user), params: { milestone: '1.0.0' } @@ -483,7 +583,7 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do create(:label_link, label: label2, target: merge_request2) end - it 'returns merge requests without any of the labels given', :aggregate_failures do + it 'returns merge requests without any of the labels given' do get api(endpoint_path, user), params: { not: { labels: ["#{label.title}, #{label2.title}"] } } expect(response).to have_gitlab_http_status(:ok) @@ -494,7 +594,7 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do end end - it 'returns merge requests without any of the milestones given', :aggregate_failures do + it 'returns merge requests without any of the milestones given' do get api(endpoint_path, user), params: { not: { milestone: milestone.title } } expect(response).to have_gitlab_http_status(:ok) @@ -505,7 +605,7 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do end end - it 'returns merge requests without the author given', :aggregate_failures do + it 'returns merge requests without the author given' do get api(endpoint_path, user), params: { not: { author_id: user2.id } } expect(response).to have_gitlab_http_status(:ok) @@ -516,7 +616,7 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do end end - it 'returns merge requests without the assignee given', :aggregate_failures do + it 'returns merge requests without the assignee given' do get api(endpoint_path, user), params: { not: { assignee_id: user2.id } } expect(response).to have_gitlab_http_status(:ok) @@ -1326,7 +1426,7 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do expect(json_response['merge_error']).to eq(merge_request.merge_error) expect(json_response['user']['can_merge']).to be_truthy expect(json_response).not_to include('rebase_in_progress') - expect(json_response['first_contribution']).to be false + expect(json_response['first_contribution']).to be true expect(json_response['has_conflicts']).to be false expect(json_response['blocking_discussions_resolved']).to be_truthy expect(json_response['references']['short']).to eq("!#{merge_request.iid}") @@ -3437,8 +3537,13 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do end describe 'POST :id/merge_requests/:merge_request_iid/subscribe' do + it_behaves_like 'POST request permissions for admin mode' do + let(:path) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe" } + let(:params) { {} } + end + it 'subscribes to a merge request' do - post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", admin) + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:created) expect(json_response['subscribed']).to eq(true) @@ -3481,7 +3586,7 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do end it 'returns 304 if not subscribed' do - post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", admin) + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_modified) end diff --git a/spec/requests/api/metadata_spec.rb b/spec/requests/api/metadata_spec.rb index b9bdadb01cc..e15186c48a5 100644 --- a/spec/requests/api/metadata_spec.rb +++ b/spec/requests/api/metadata_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Metadata, feature_category: :not_owned do +RSpec.describe API::Metadata, feature_category: :shared do shared_examples_for 'GET /metadata' do context 'when unauthenticated' do it 'returns authentication error' do diff --git a/spec/requests/api/metrics/dashboard/annotations_spec.rb b/spec/requests/api/metrics/dashboard/annotations_spec.rb index 7932dd29e4d..250fe2a3ee3 100644 --- a/spec/requests/api/metrics/dashboard/annotations_spec.rb +++ b/spec/requests/api/metrics/dashboard/annotations_spec.rb @@ -16,6 +16,7 @@ RSpec.describe API::Metrics::Dashboard::Annotations, feature_category: :metrics let(:url) { "/#{source_type.pluralize}/#{source.id}/metrics_dashboard/annotations" } before do + stub_feature_flags(remove_monitor_metrics: false) project.add_developer(user) end @@ -35,7 +36,7 @@ RSpec.describe API::Metrics::Dashboard::Annotations, feature_category: :metrics end context 'with invalid parameters' do - it 'returns error messsage' do + it 'returns error message' do post api(url, user), params: { dashboard_path: '', starting_at: nil, description: nil } expect(response).to have_gitlab_http_status(:bad_request) @@ -104,6 +105,18 @@ RSpec.describe API::Metrics::Dashboard::Annotations, feature_category: :metrics expect(response).to have_gitlab_http_status(:forbidden) end end + + context 'when metrics dashboard feature is unavailable' do + before do + stub_feature_flags(remove_monitor_metrics: true) + end + + it 'returns 404 not found' do + post api(url, user), params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + end end end diff --git a/spec/requests/api/metrics/user_starred_dashboards_spec.rb b/spec/requests/api/metrics/user_starred_dashboards_spec.rb index 38d3c0be8b2..6fc98de0777 100644 --- a/spec/requests/api/metrics/user_starred_dashboards_spec.rb +++ b/spec/requests/api/metrics/user_starred_dashboards_spec.rb @@ -15,6 +15,10 @@ RSpec.describe API::Metrics::UserStarredDashboards, feature_category: :metrics d } end + before do + stub_feature_flags(remove_monitor_metrics: false) + end + describe 'POST /projects/:id/metrics/user_starred_dashboards' do before do project.add_reporter(user) @@ -84,6 +88,18 @@ RSpec.describe API::Metrics::UserStarredDashboards, feature_category: :metrics d expect(response).to have_gitlab_http_status(:not_found) end end + + context 'when metrics dashboard feature is unavailable' do + before do + stub_feature_flags(remove_monitor_metrics: true) + end + + it 'returns 404 not found' do + post api(url, user), params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + end end describe 'DELETE /projects/:id/metrics/user_starred_dashboards' do @@ -161,5 +177,17 @@ RSpec.describe API::Metrics::UserStarredDashboards, feature_category: :metrics d expect(response).to have_gitlab_http_status(:not_found) end end + + context 'when metrics dashboard feature is unavailable' do + before do + stub_feature_flags(remove_monitor_metrics: true) + end + + it 'returns 404 not found' do + delete api(url, user), params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + end end end diff --git a/spec/requests/api/ml/mlflow/experiments_spec.rb b/spec/requests/api/ml/mlflow/experiments_spec.rb new file mode 100644 index 00000000000..1a2577e69e7 --- /dev/null +++ b/spec/requests/api/ml/mlflow/experiments_spec.rb @@ -0,0 +1,215 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ml::Mlflow::Experiments, feature_category: :mlops do + let_it_be(:project) { create(:project) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:another_project) { build(:project).tap { |p| p.add_developer(developer) } } + let_it_be(:experiment) do + create(:ml_experiments, :with_metadata, project: project) + end + + let_it_be(:tokens) do + { + write: create(:personal_access_token, scopes: %w[read_api api], user: developer), + read: create(:personal_access_token, scopes: %w[read_api], user: developer), + no_access: create(:personal_access_token, scopes: %w[read_user], user: developer), + different_user: create(:personal_access_token, scopes: %w[read_api api], user: build(:user)) + } + end + + let(:current_user) { developer } + let(:ff_value) { true } + let(:access_token) { tokens[:write] } + let(:headers) { { 'Authorization' => "Bearer #{access_token.token}" } } + let(:project_id) { project.id } + let(:default_params) { {} } + let(:params) { default_params } + let(:request) { get api(route), params: params, headers: headers } + let(:json_response) { Gitlab::Json.parse(api_response.body) } + let(:presented_experiment) do + { + 'experiment_id' => experiment.iid.to_s, + 'name' => experiment.name, + 'lifecycle_stage' => 'active', + 'artifact_location' => 'not_implemented', + 'tags' => [ + { + 'key' => experiment.metadata[0].name, + 'value' => experiment.metadata[0].value + }, + { + 'key' => experiment.metadata[1].name, + 'value' => experiment.metadata[1].value + } + ] + } + end + + subject(:api_response) do + request + response + end + + before do + stub_feature_flags(ml_experiment_tracking: ff_value) + end + + describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/get' do + let(:experiment_iid) { experiment.iid.to_s } + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/get?experiment_id=#{experiment_iid}" } + + it 'returns the experiment', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + is_expected.to match_response_schema('ml/get_experiment') + expect(json_response).to include({ 'experiment' => presented_experiment }) + end + + describe 'Error States' do + context 'when has access' do + context 'and experiment does not exist' do + let(:experiment_iid) { non_existing_record_iid.to_s } + + it_behaves_like 'MLflow|Not Found - Resource Does Not Exist' + end + + context 'and experiment_id is not passed' do + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/get" } + + it_behaves_like 'MLflow|Not Found - Resource Does Not Exist' + end + end + + it_behaves_like 'MLflow|shared error cases' + it_behaves_like 'MLflow|Requires read_api scope' + end + end + + describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/list' do + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/list" } + + it 'returns the experiments', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + is_expected.to match_response_schema('ml/list_experiments') + expect(json_response).to include({ 'experiments' => [presented_experiment] }) + end + + context 'when there are no experiments' do + let(:project_id) { another_project.id } + + it 'returns an empty list' do + expect(json_response).to include({ 'experiments' => [] }) + end + end + + describe 'Error States' do + it_behaves_like 'MLflow|shared error cases' + it_behaves_like 'MLflow|Requires read_api scope' + end + end + + describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/get-by-name' do + let(:experiment_name) { experiment.name } + let(:route) do + "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/get-by-name?experiment_name=#{experiment_name}" + end + + it 'returns the experiment', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + is_expected.to match_response_schema('ml/get_experiment') + expect(json_response).to include({ 'experiment' => presented_experiment }) + end + + describe 'Error States' do + context 'when has access but experiment does not exist' do + let(:experiment_name) { "random_experiment" } + + it_behaves_like 'MLflow|Not Found - Resource Does Not Exist' + end + + context 'when has access but experiment_name is not passed' do + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/get-by-name" } + + it_behaves_like 'MLflow|Not Found - Resource Does Not Exist' + end + + it_behaves_like 'MLflow|shared error cases' + it_behaves_like 'MLflow|Requires read_api scope' + end + end + + describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/create' do + let(:route) do + "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/create" + end + + let(:params) { { name: 'new_experiment' } } + let(:request) { post api(route), params: params, headers: headers } + + it 'creates the experiment', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + expect(json_response).to include('experiment_id') + end + + describe 'Error States' do + context 'when experiment name is not passed' do + let(:params) { {} } + + it_behaves_like 'MLflow|Bad Request' + end + + context 'when experiment name already exists' do + let(:existing_experiment) do + create(:ml_experiments, user: current_user, project: project) + end + + let(:params) { { name: existing_experiment.name } } + + it "is Bad Request", :aggregate_failures do + is_expected.to have_gitlab_http_status(:bad_request) + + expect(json_response).to include({ 'error_code' => 'RESOURCE_ALREADY_EXISTS' }) + end + end + + context 'when project does not exist' do + let(:route) { "/projects/#{non_existing_record_id}/ml/mlflow/api/2.0/mlflow/experiments/create" } + + it "is Not Found", :aggregate_failures do + is_expected.to have_gitlab_http_status(:not_found) + + expect(json_response['message']).to eq('404 Project Not Found') + end + end + + it_behaves_like 'MLflow|shared error cases' + it_behaves_like 'MLflow|Requires api scope' + end + end + + describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/set-experiment-tag' do + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/set-experiment-tag" } + let(:default_params) { { experiment_id: experiment.iid.to_s, key: 'some_key', value: 'value' } } + let(:params) { default_params } + let(:request) { post api(route), params: params, headers: headers } + + it 'logs the tag', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + expect(json_response).to be_empty + expect(experiment.reload.metadata.map(&:name)).to include('some_key') + end + + describe 'Error Cases' do + context 'when tag was already set' do + let(:params) { default_params.merge(key: experiment.metadata[0].name) } + + it_behaves_like 'MLflow|Bad Request' + end + + it_behaves_like 'MLflow|shared error cases' + it_behaves_like 'MLflow|Requires api scope' + it_behaves_like 'MLflow|Bad Request on missing required', [:key, :value] + end + end +end diff --git a/spec/requests/api/ml/mlflow/runs_spec.rb b/spec/requests/api/ml/mlflow/runs_spec.rb new file mode 100644 index 00000000000..746372b7978 --- /dev/null +++ b/spec/requests/api/ml/mlflow/runs_spec.rb @@ -0,0 +1,354 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ml::Mlflow::Runs, feature_category: :mlops do + let_it_be(:project) { create(:project) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:another_project) { build(:project).tap { |p| p.add_developer(developer) } } + let_it_be(:experiment) do + create(:ml_experiments, :with_metadata, project: project) + end + + let_it_be(:candidate) do + create(:ml_candidates, + :with_metrics_and_params, :with_metadata, + user: experiment.user, start_time: 1234, experiment: experiment, project: project) + end + + let_it_be(:tokens) do + { + write: create(:personal_access_token, scopes: %w[read_api api], user: developer), + read: create(:personal_access_token, scopes: %w[read_api], user: developer), + no_access: create(:personal_access_token, scopes: %w[read_user], user: developer), + different_user: create(:personal_access_token, scopes: %w[read_api api], user: build(:user)) + } + end + + let(:current_user) { developer } + let(:ff_value) { true } + let(:access_token) { tokens[:write] } + let(:headers) { { 'Authorization' => "Bearer #{access_token.token}" } } + let(:project_id) { project.id } + let(:default_params) { {} } + let(:params) { default_params } + let(:request) { get api(route), params: params, headers: headers } + let(:json_response) { Gitlab::Json.parse(api_response.body) } + + subject(:api_response) do + request + response + end + + before do + stub_feature_flags(ml_experiment_tracking: ff_value) + end + + RSpec.shared_examples 'MLflow|run_id param error cases' do + context 'when run id is not passed' do + let(:params) { {} } + + it "is Bad Request" do + is_expected.to have_gitlab_http_status(:bad_request) + end + end + + context 'when run_id is invalid' do + let(:params) { default_params.merge(run_id: non_existing_record_iid.to_s) } + + it "is Resource Does Not Exist", :aggregate_failures do + is_expected.to have_gitlab_http_status(:not_found) + + expect(json_response).to include({ "error_code" => 'RESOURCE_DOES_NOT_EXIST' }) + end + end + + context 'when run_id is not in in the project' do + let(:project_id) { another_project.id } + + it "is Resource Does Not Exist", :aggregate_failures do + is_expected.to have_gitlab_http_status(:not_found) + + expect(json_response).to include({ "error_code" => 'RESOURCE_DOES_NOT_EXIST' }) + end + end + end + + describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/create' do + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/create" } + let(:params) do + { + experiment_id: experiment.iid.to_s, + start_time: Time.now.to_i, + run_name: "A new Run", + tags: [ + { key: 'hello', value: 'world' } + ] + } + end + + let(:request) { post api(route), params: params, headers: headers } + + it 'creates the run', :aggregate_failures do + expected_properties = { + 'experiment_id' => params[:experiment_id], + 'user_id' => current_user.id.to_s, + 'run_name' => "A new Run", + 'start_time' => params[:start_time], + 'status' => 'RUNNING', + 'lifecycle_stage' => 'active' + } + + is_expected.to have_gitlab_http_status(:ok) + is_expected.to match_response_schema('ml/run') + expect(json_response['run']).to include('info' => hash_including(**expected_properties), + 'data' => { + 'metrics' => [], + 'params' => [], + 'tags' => [{ 'key' => 'hello', 'value' => 'world' }] + }) + end + + describe 'Error States' do + context 'when experiment id is not passed' do + let(:params) { {} } + + it_behaves_like 'MLflow|Bad Request' + end + + context 'when experiment id does not exist' do + let(:params) { { experiment_id: non_existing_record_iid.to_s } } + + it_behaves_like 'MLflow|Not Found - Resource Does Not Exist' + end + + context 'when experiment exists but is not part of the project' do + let(:project_id) { another_project.id } + + it_behaves_like 'MLflow|Not Found - Resource Does Not Exist' + end + + it_behaves_like 'MLflow|shared error cases' + it_behaves_like 'MLflow|Requires api scope' + end + end + + describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/runs/get' do + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/get" } + let(:default_params) { { 'run_id' => candidate.eid } } + + it 'gets the run', :aggregate_failures do + expected_properties = { + 'experiment_id' => candidate.experiment.iid.to_s, + 'user_id' => candidate.user.id.to_s, + 'start_time' => candidate.start_time, + 'artifact_uri' => "http://www.example.com/api/v4/projects/#{project_id}/packages/generic/ml_experiment_#{experiment.iid}/#{candidate.iid}/", + 'status' => "RUNNING", + 'lifecycle_stage' => "active" + } + + is_expected.to have_gitlab_http_status(:ok) + is_expected.to match_response_schema('ml/run') + expect(json_response['run']).to include( + 'info' => hash_including(**expected_properties), + 'data' => { + 'metrics' => [ + hash_including('key' => candidate.metrics[0].name), + hash_including('key' => candidate.metrics[1].name) + ], + 'params' => [ + { 'key' => candidate.params[0].name, 'value' => candidate.params[0].value }, + { 'key' => candidate.params[1].name, 'value' => candidate.params[1].value } + ], + 'tags' => [ + { 'key' => candidate.metadata[0].name, 'value' => candidate.metadata[0].value }, + { 'key' => candidate.metadata[1].name, 'value' => candidate.metadata[1].value } + ] + }) + end + + describe 'Error States' do + it_behaves_like 'MLflow|run_id param error cases' + it_behaves_like 'MLflow|shared error cases' + it_behaves_like 'MLflow|Requires read_api scope' + end + end + + describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/update' do + let(:default_params) { { run_id: candidate.eid.to_s, status: 'FAILED', end_time: Time.now.to_i } } + let(:request) { post api(route), params: params, headers: headers } + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/update" } + + it 'updates the run', :aggregate_failures do + expected_properties = { + 'experiment_id' => candidate.experiment.iid.to_s, + 'user_id' => candidate.user.id.to_s, + 'start_time' => candidate.start_time, + 'end_time' => params[:end_time], + 'artifact_uri' => "http://www.example.com/api/v4/projects/#{project_id}/packages/generic/ml_experiment_#{experiment.iid}/#{candidate.iid}/", + 'status' => 'FAILED', + 'lifecycle_stage' => 'active' + } + + is_expected.to have_gitlab_http_status(:ok) + is_expected.to match_response_schema('ml/update_run') + expect(json_response).to include('run_info' => hash_including(**expected_properties)) + end + + describe 'Error States' do + context 'when status in invalid' do + let(:params) { default_params.merge(status: 'YOLO') } + + it_behaves_like 'MLflow|Bad Request' + end + + context 'when end_time is invalid' do + let(:params) { default_params.merge(end_time: 's') } + + it_behaves_like 'MLflow|Bad Request' + end + + it_behaves_like 'MLflow|shared error cases' + it_behaves_like 'MLflow|Requires api scope' + it_behaves_like 'MLflow|run_id param error cases' + end + end + + describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/log-metric' do + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/log-metric" } + let(:default_params) { { run_id: candidate.eid.to_s, key: 'some_key', value: 10.0, timestamp: Time.now.to_i } } + let(:request) { post api(route), params: params, headers: headers } + + it 'logs the metric', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + expect(json_response).to be_empty + expect(candidate.metrics.reload.length).to eq(3) + end + + describe 'Error Cases' do + it_behaves_like 'MLflow|shared error cases' + it_behaves_like 'MLflow|Requires api scope' + it_behaves_like 'MLflow|run_id param error cases' + it_behaves_like 'MLflow|Bad Request on missing required', [:key, :value, :timestamp] + end + end + + describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/log-parameter' do + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/log-parameter" } + let(:default_params) { { run_id: candidate.eid.to_s, key: 'some_key', value: 'value' } } + let(:request) { post api(route), params: params, headers: headers } + + it 'logs the parameter', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + expect(json_response).to be_empty + expect(candidate.params.reload.length).to eq(3) + end + + describe 'Error Cases' do + context 'when parameter was already logged' do + let(:params) { default_params.tap { |p| p[:key] = candidate.params[0].name } } + + it_behaves_like 'MLflow|Bad Request' + end + + it_behaves_like 'MLflow|shared error cases' + it_behaves_like 'MLflow|Requires api scope' + it_behaves_like 'MLflow|run_id param error cases' + it_behaves_like 'MLflow|Bad Request on missing required', [:key, :value] + end + end + + describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/set-tag' do + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/set-tag" } + let(:default_params) { { run_id: candidate.eid.to_s, key: 'some_key', value: 'value' } } + let(:request) { post api(route), params: params, headers: headers } + + it 'logs the tag', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + expect(json_response).to be_empty + expect(candidate.reload.metadata.map(&:name)).to include('some_key') + end + + describe 'Error Cases' do + context 'when tag was already logged' do + let(:params) { default_params.tap { |p| p[:key] = candidate.metadata[0].name } } + + it_behaves_like 'MLflow|Bad Request' + end + + it_behaves_like 'MLflow|shared error cases' + it_behaves_like 'MLflow|Requires api scope' + it_behaves_like 'MLflow|run_id param error cases' + it_behaves_like 'MLflow|Bad Request on missing required', [:key, :value] + end + end + + describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/log-batch' do + let_it_be(:candidate2) do + create(:ml_candidates, user: experiment.user, start_time: 1234, experiment: experiment, project: project) + end + + let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/log-batch" } + let(:default_params) do + { + run_id: candidate2.eid.to_s, + metrics: [ + { key: 'mae', value: 2.5, timestamp: 1552550804 }, + { key: 'rmse', value: 2.7, timestamp: 1552550804 } + ], + params: [{ key: 'model_class', value: 'LogisticRegression' }], + tags: [{ key: 'tag1', value: 'tag.value.1' }] + } + end + + let(:request) { post api(route), params: params, headers: headers } + + it 'logs parameters and metrics', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + expect(json_response).to be_empty + expect(candidate2.params.size).to eq(1) + expect(candidate2.metadata.size).to eq(1) + expect(candidate2.metrics.size).to eq(2) + end + + context 'when parameter was already logged' do + let(:params) do + default_params.tap { |p| p[:params] = [{ key: 'hello', value: 'a' }, { key: 'hello', value: 'b' }] } + end + + it 'does not log', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + expect(candidate2.params.reload.size).to eq(1) + end + end + + context 'when tag was already logged' do + let(:params) do + default_params.tap { |p| p[:tags] = [{ key: 'tag1', value: 'a' }, { key: 'tag1', value: 'b' }] } + end + + it 'logs only 1', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + expect(candidate2.metadata.reload.size).to eq(1) + end + end + + describe 'Error Cases' do + context 'when required metric key is missing' do + let(:params) { default_params.tap { |p| p[:metrics] = [p[:metrics][0].delete(:key)] } } + + it_behaves_like 'MLflow|Bad Request' + end + + context 'when required param key is missing' do + let(:params) { default_params.tap { |p| p[:params] = [p[:params][0].delete(:key)] } } + + it_behaves_like 'MLflow|Bad Request' + end + + it_behaves_like 'MLflow|shared error cases' + it_behaves_like 'MLflow|Requires api scope' + it_behaves_like 'MLflow|run_id param error cases' + end + end +end diff --git a/spec/requests/api/ml/mlflow_spec.rb b/spec/requests/api/ml/mlflow_spec.rb deleted file mode 100644 index fdf115f7e92..00000000000 --- a/spec/requests/api/ml/mlflow_spec.rb +++ /dev/null @@ -1,630 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'mime/types' - -RSpec.describe API::Ml::Mlflow, feature_category: :mlops do - include SessionHelpers - include ApiHelpers - include HttpBasicAuthHelpers - - let_it_be(:project) { create(:project, :private) } - let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } - let_it_be(:another_project) { build(:project).tap { |p| p.add_developer(developer) } } - let_it_be(:experiment) do - create(:ml_experiments, :with_metadata, project: project) - end - - let_it_be(:candidate) do - create(:ml_candidates, - :with_metrics_and_params, :with_metadata, - user: experiment.user, start_time: 1234, experiment: experiment) - end - - let_it_be(:tokens) do - { - write: create(:personal_access_token, scopes: %w[read_api api], user: developer), - read: create(:personal_access_token, scopes: %w[read_api], user: developer), - no_access: create(:personal_access_token, scopes: %w[read_user], user: developer), - different_user: create(:personal_access_token, scopes: %w[read_api api], user: build(:user)) - } - end - - let(:current_user) { developer } - let(:ff_value) { true } - let(:access_token) { tokens[:write] } - let(:headers) do - { 'Authorization' => "Bearer #{access_token.token}" } - end - - let(:project_id) { project.id } - let(:default_params) { {} } - let(:params) { default_params } - let(:request) { get api(route), params: params, headers: headers } - - before do - stub_feature_flags(ml_experiment_tracking: ff_value) - - request - end - - shared_examples 'Not Found' do |message| - it "is Not Found" do - expect(response).to have_gitlab_http_status(:not_found) - - expect(json_response['message']).to eq(message) if message.present? - end - end - - shared_examples 'Not Found - Resource Does Not Exist' do - it "is Resource Does Not Exist" do - expect(response).to have_gitlab_http_status(:not_found) - - expect(json_response).to include({ "error_code" => 'RESOURCE_DOES_NOT_EXIST' }) - end - end - - shared_examples 'Requires api scope' do - context 'when user has access but token has wrong scope' do - let(:access_token) { tokens[:read] } - - it { expect(response).to have_gitlab_http_status(:forbidden) } - end - end - - shared_examples 'Requires read_api scope' do - context 'when user has access but token has wrong scope' do - let(:access_token) { tokens[:no_access] } - - it { expect(response).to have_gitlab_http_status(:forbidden) } - end - end - - shared_examples 'Bad Request' do |error_code = nil| - it "is Bad Request" do - expect(response).to have_gitlab_http_status(:bad_request) - - expect(json_response).to include({ 'error_code' => error_code }) if error_code.present? - end - end - - shared_examples 'shared error cases' do - context 'when not authenticated' do - let(:headers) { {} } - - it "is Unauthorized" do - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - - context 'when user does not have access' do - let(:access_token) { tokens[:different_user] } - - it_behaves_like 'Not Found' - end - - context 'when ff is disabled' do - let(:ff_value) { false } - - it_behaves_like 'Not Found' - end - end - - shared_examples 'run_id param error cases' do - context 'when run id is not passed' do - let(:params) { {} } - - it_behaves_like 'Bad Request' - end - - context 'when run_id is invalid' do - let(:params) { default_params.merge(run_id: non_existing_record_iid.to_s) } - - it_behaves_like 'Not Found - Resource Does Not Exist' - end - - context 'when run_id is not in in the project' do - let(:project_id) { another_project.id } - - it_behaves_like 'Not Found - Resource Does Not Exist' - end - end - - shared_examples 'Bad Request on missing required' do |keys| - keys.each do |key| - context "when \"#{key}\" is missing" do - let(:params) { default_params.tap { |p| p.delete(key) } } - - it_behaves_like 'Bad Request' - end - end - end - - describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/get' do - let(:experiment_iid) { experiment.iid.to_s } - let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/get?experiment_id=#{experiment_iid}" } - - it 'returns the experiment', :aggregate_failures do - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('ml/get_experiment') - expect(json_response).to include({ - 'experiment' => { - 'experiment_id' => experiment_iid, - 'name' => experiment.name, - 'lifecycle_stage' => 'active', - 'artifact_location' => 'not_implemented', - 'tags' => [ - { - 'key' => experiment.metadata[0].name, - 'value' => experiment.metadata[0].value - }, - { - 'key' => experiment.metadata[1].name, - 'value' => experiment.metadata[1].value - } - ] - } - }) - end - - describe 'Error States' do - context 'when has access' do - context 'and experiment does not exist' do - let(:experiment_iid) { non_existing_record_iid.to_s } - - it_behaves_like 'Not Found - Resource Does Not Exist' - end - - context 'and experiment_id is not passed' do - let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/get" } - - it_behaves_like 'Not Found - Resource Does Not Exist' - end - end - - it_behaves_like 'shared error cases' - it_behaves_like 'Requires read_api scope' - end - end - - describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/list' do - let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/list" } - - it 'returns the experiments' do - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('ml/list_experiments') - expect(json_response).to include({ - 'experiments' => [ - 'experiment_id' => experiment.iid.to_s, - 'name' => experiment.name, - 'lifecycle_stage' => 'active', - 'artifact_location' => 'not_implemented', - 'tags' => [ - { - 'key' => experiment.metadata[0].name, - 'value' => experiment.metadata[0].value - }, - { - 'key' => experiment.metadata[1].name, - 'value' => experiment.metadata[1].value - } - ] - ] - }) - end - - context 'when there are no experiments' do - let(:project_id) { another_project.id } - - it 'returns an empty list' do - expect(json_response).to include({ 'experiments' => [] }) - end - end - - describe 'Error States' do - it_behaves_like 'shared error cases' - it_behaves_like 'Requires read_api scope' - end - end - - describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/get-by-name' do - let(:experiment_name) { experiment.name } - let(:route) do - "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/get-by-name?experiment_name=#{experiment_name}" - end - - it 'returns the experiment', :aggregate_failures do - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('ml/get_experiment') - expect(json_response).to include({ - 'experiment' => { - 'experiment_id' => experiment.iid.to_s, - 'name' => experiment_name, - 'lifecycle_stage' => 'active', - 'artifact_location' => 'not_implemented', - 'tags' => [ - { - 'key' => experiment.metadata[0].name, - 'value' => experiment.metadata[0].value - }, - { - 'key' => experiment.metadata[1].name, - 'value' => experiment.metadata[1].value - } - ] - } - }) - end - - describe 'Error States' do - context 'when has access but experiment does not exist' do - let(:experiment_name) { "random_experiment" } - - it_behaves_like 'Not Found - Resource Does Not Exist' - end - - context 'when has access but experiment_name is not passed' do - let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/get-by-name" } - - it_behaves_like 'Not Found - Resource Does Not Exist' - end - - it_behaves_like 'shared error cases' - it_behaves_like 'Requires read_api scope' - end - end - - describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/create' do - let(:route) do - "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/create" - end - - let(:params) { { name: 'new_experiment' } } - let(:request) { post api(route), params: params, headers: headers } - - it 'creates the experiment', :aggregate_failures do - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to include('experiment_id') - end - - describe 'Error States' do - context 'when experiment name is not passed' do - let(:params) { {} } - - it_behaves_like 'Bad Request' - end - - context 'when experiment name already exists' do - let(:existing_experiment) do - create(:ml_experiments, user: current_user, project: project) - end - - let(:params) { { name: existing_experiment.name } } - - it_behaves_like 'Bad Request', 'RESOURCE_ALREADY_EXISTS' - end - - context 'when project does not exist' do - let(:route) { "/projects/#{non_existing_record_id}/ml/mlflow/api/2.0/mlflow/experiments/create" } - - it_behaves_like 'Not Found', '404 Project Not Found' - end - - it_behaves_like 'shared error cases' - it_behaves_like 'Requires api scope' - end - end - - describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/set-experiment-tag' do - let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/set-experiment-tag" } - let(:default_params) { { experiment_id: experiment.iid.to_s, key: 'some_key', value: 'value' } } - let(:params) { default_params } - let(:request) { post api(route), params: params, headers: headers } - - it 'logs the tag', :aggregate_failures do - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_empty - expect(experiment.reload.metadata.map(&:name)).to include('some_key') - end - - describe 'Error Cases' do - context 'when tag was already set' do - let(:params) { default_params.merge(key: experiment.metadata[0].name) } - - it_behaves_like 'Bad Request' - end - - it_behaves_like 'shared error cases' - it_behaves_like 'Requires api scope' - it_behaves_like 'Bad Request on missing required', [:key, :value] - end - end - - describe 'Runs' do - describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/create' do - let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/create" } - let(:params) do - { - experiment_id: experiment.iid.to_s, - start_time: Time.now.to_i, - run_name: "A new Run", - tags: [ - { key: 'hello', value: 'world' } - ] - } - end - - let(:request) { post api(route), params: params, headers: headers } - - it 'creates the run', :aggregate_failures do - expected_properties = { - 'experiment_id' => params[:experiment_id], - 'user_id' => current_user.id.to_s, - 'run_name' => "A new Run", - 'start_time' => params[:start_time], - 'status' => 'RUNNING', - 'lifecycle_stage' => 'active' - } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('ml/run') - expect(json_response['run']).to include('info' => hash_including(**expected_properties), - 'data' => { - 'metrics' => [], - 'params' => [], - 'tags' => [{ 'key' => 'hello', 'value' => 'world' }] - }) - end - - describe 'Error States' do - context 'when experiment id is not passed' do - let(:params) { {} } - - it_behaves_like 'Bad Request' - end - - context 'when experiment id does not exist' do - let(:params) { { experiment_id: non_existing_record_iid.to_s } } - - it_behaves_like 'Not Found - Resource Does Not Exist' - end - - context 'when experiment exists but is not part of the project' do - let(:project_id) { another_project.id } - - it_behaves_like 'Not Found - Resource Does Not Exist' - end - - it_behaves_like 'shared error cases' - it_behaves_like 'Requires api scope' - end - end - - describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/runs/get' do - let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/get" } - let(:default_params) { { 'run_id' => candidate.iid } } - - it 'gets the run', :aggregate_failures do - expected_properties = { - 'experiment_id' => candidate.experiment.iid.to_s, - 'user_id' => candidate.user.id.to_s, - 'start_time' => candidate.start_time, - 'artifact_uri' => "http://www.example.com/api/v4/projects/#{project_id}/packages/generic/ml_candidate_#{candidate.id}/-/", - 'status' => "RUNNING", - 'lifecycle_stage' => "active" - } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('ml/run') - expect(json_response['run']).to include( - 'info' => hash_including(**expected_properties), - 'data' => { - 'metrics' => [ - hash_including('key' => candidate.metrics[0].name), - hash_including('key' => candidate.metrics[1].name) - ], - 'params' => [ - { 'key' => candidate.params[0].name, 'value' => candidate.params[0].value }, - { 'key' => candidate.params[1].name, 'value' => candidate.params[1].value } - ], - 'tags' => [ - { 'key' => candidate.metadata[0].name, 'value' => candidate.metadata[0].value }, - { 'key' => candidate.metadata[1].name, 'value' => candidate.metadata[1].value } - ] - }) - end - - describe 'Error States' do - it_behaves_like 'run_id param error cases' - it_behaves_like 'shared error cases' - it_behaves_like 'Requires read_api scope' - end - end - - describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/update' do - let(:default_params) { { run_id: candidate.iid.to_s, status: 'FAILED', end_time: Time.now.to_i } } - let(:request) { post api(route), params: params, headers: headers } - let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/update" } - - it 'updates the run', :aggregate_failures do - expected_properties = { - 'experiment_id' => candidate.experiment.iid.to_s, - 'user_id' => candidate.user.id.to_s, - 'start_time' => candidate.start_time, - 'end_time' => params[:end_time], - 'artifact_uri' => "http://www.example.com/api/v4/projects/#{project_id}/packages/generic/ml_candidate_#{candidate.id}/-/", - 'status' => 'FAILED', - 'lifecycle_stage' => 'active' - } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('ml/update_run') - expect(json_response).to include('run_info' => hash_including(**expected_properties)) - end - - describe 'Error States' do - context 'when status in invalid' do - let(:params) { default_params.merge(status: 'YOLO') } - - it_behaves_like 'Bad Request' - end - - context 'when end_time is invalid' do - let(:params) { default_params.merge(end_time: 's') } - - it_behaves_like 'Bad Request' - end - - it_behaves_like 'shared error cases' - it_behaves_like 'Requires api scope' - it_behaves_like 'run_id param error cases' - end - end - - describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/log-metric' do - let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/log-metric" } - let(:default_params) { { run_id: candidate.iid.to_s, key: 'some_key', value: 10.0, timestamp: Time.now.to_i } } - let(:request) { post api(route), params: params, headers: headers } - - it 'logs the metric', :aggregate_failures do - candidate.metrics.reload - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_empty - expect(candidate.metrics.length).to eq(3) - end - - describe 'Error Cases' do - it_behaves_like 'shared error cases' - it_behaves_like 'Requires api scope' - it_behaves_like 'run_id param error cases' - it_behaves_like 'Bad Request on missing required', [:key, :value, :timestamp] - end - end - - describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/log-parameter' do - let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/log-parameter" } - let(:default_params) { { run_id: candidate.iid.to_s, key: 'some_key', value: 'value' } } - let(:request) { post api(route), params: params, headers: headers } - - it 'logs the parameter', :aggregate_failures do - candidate.params.reload - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_empty - expect(candidate.params.length).to eq(3) - end - - describe 'Error Cases' do - context 'when parameter was already logged' do - let(:params) { default_params.tap { |p| p[:key] = candidate.params[0].name } } - - it_behaves_like 'Bad Request' - end - - it_behaves_like 'shared error cases' - it_behaves_like 'Requires api scope' - it_behaves_like 'run_id param error cases' - it_behaves_like 'Bad Request on missing required', [:key, :value] - end - end - - describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/set-tag' do - let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/set-tag" } - let(:default_params) { { run_id: candidate.iid.to_s, key: 'some_key', value: 'value' } } - let(:request) { post api(route), params: params, headers: headers } - - it 'logs the tag', :aggregate_failures do - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_empty - expect(candidate.reload.metadata.map(&:name)).to include('some_key') - end - - describe 'Error Cases' do - context 'when tag was already logged' do - let(:params) { default_params.tap { |p| p[:key] = candidate.metadata[0].name } } - - it_behaves_like 'Bad Request' - end - - it_behaves_like 'shared error cases' - it_behaves_like 'Requires api scope' - it_behaves_like 'run_id param error cases' - it_behaves_like 'Bad Request on missing required', [:key, :value] - end - end - - describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/log-batch' do - let(:candidate2) do - create(:ml_candidates, user: experiment.user, start_time: 1234, experiment: experiment) - end - - let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/log-batch" } - let(:default_params) do - { - run_id: candidate2.iid.to_s, - metrics: [ - { key: 'mae', value: 2.5, timestamp: 1552550804 }, - { key: 'rmse', value: 2.7, timestamp: 1552550804 } - ], - params: [{ key: 'model_class', value: 'LogisticRegression' }], - tags: [{ key: 'tag1', value: 'tag.value.1' }] - } - end - - let(:request) { post api(route), params: params, headers: headers } - - it 'logs parameters and metrics', :aggregate_failures do - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_empty - expect(candidate2.params.size).to eq(1) - expect(candidate2.metadata.size).to eq(1) - expect(candidate2.metrics.size).to eq(2) - end - - context 'when parameter was already logged' do - let(:params) do - default_params.tap { |p| p[:params] = [{ key: 'hello', value: 'a' }, { key: 'hello', value: 'b' }] } - end - - it 'does not log', :aggregate_failures do - candidate.params.reload - - expect(response).to have_gitlab_http_status(:ok) - expect(candidate2.params.size).to eq(1) - end - end - - context 'when tag was already logged' do - let(:params) do - default_params.tap { |p| p[:tags] = [{ key: 'tag1', value: 'a' }, { key: 'tag1', value: 'b' }] } - end - - it 'logs only 1', :aggregate_failures do - candidate.metadata.reload - - expect(response).to have_gitlab_http_status(:ok) - expect(candidate2.metadata.size).to eq(1) - end - end - - describe 'Error Cases' do - context 'when required metric key is missing' do - let(:params) { default_params.tap { |p| p[:metrics] = [p[:metrics][0].delete(:key)] } } - - it_behaves_like 'Bad Request' - end - - context 'when required param key is missing' do - let(:params) { default_params.tap { |p| p[:params] = [p[:params][0].delete(:key)] } } - - it_behaves_like 'Bad Request' - end - - it_behaves_like 'shared error cases' - it_behaves_like 'Requires api scope' - it_behaves_like 'run_id param error cases' - end - end - end -end diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index 44574caf54a..f268a092034 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -2,25 +2,27 @@ require 'spec_helper' -RSpec.describe API::Namespaces, feature_category: :subgroups do +RSpec.describe API::Namespaces, :aggregate_failures, feature_category: :subgroups do let_it_be(:admin) { create(:admin) } let_it_be(:user) { create(:user) } let_it_be(:group1) { create(:group, name: 'group.one') } let_it_be(:group2) { create(:group, :nested) } let_it_be(:project) { create(:project, namespace: group2, name: group2.name, path: group2.path) } let_it_be(:project_namespace) { project.project_namespace } + let_it_be(:path) { "/namespaces" } describe "GET /namespaces" do context "when unauthenticated" do it "returns authentication error" do - get api("/namespaces") + get api(path) + expect(response).to have_gitlab_http_status(:unauthorized) end end context "when authenticated as admin" do it "returns correct attributes" do - get api("/namespaces", admin) + get api(path, admin, admin_mode: true) group_kind_json_response = json_response.find { |resource| resource['kind'] == 'group' } user_kind_json_response = json_response.find { |resource| resource['kind'] == 'user' } @@ -34,7 +36,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do end it "admin: returns an array of all namespaces" do - get api("/namespaces", admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -44,7 +46,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do end it "admin: returns an array of matched namespaces" do - get api("/namespaces?search=#{group2.name}", admin) + get api("/namespaces?search=#{group2.name}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -59,7 +61,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do it "returns correct attributes when user can admin group" do group1.add_owner(user) - get api("/namespaces", user) + get api(path, user) owned_group_response = json_response.find { |resource| resource['id'] == group1.id } @@ -70,7 +72,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do it "returns correct attributes when user cannot admin group" do group1.add_guest(user) - get api("/namespaces", user) + get api(path, user) guest_group_response = json_response.find { |resource| resource['id'] == group1.id } @@ -78,7 +80,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do end it "user: returns an array of namespaces" do - get api("/namespaces", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -115,9 +117,19 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do let_it_be(:user2) { create(:user) } - shared_examples 'can access namespace' do + it_behaves_like 'GET request permissions for admin mode' do + let(:path) { "/namespaces/#{group2.id}" } + let(:failed_status_code) { :not_found } + end + + it_behaves_like 'GET request permissions for admin mode' do + let(:path) { "/namespaces/#{user2.namespace.id}" } + let(:failed_status_code) { :not_found } + end + + shared_examples 'can access namespace' do |admin_mode: false| it 'returns namespace details' do - get api("/namespaces/#{namespace_id}", request_actor) + get api("#{path}/#{namespace_id}", request_actor, admin_mode: admin_mode) expect(response).to have_gitlab_http_status(:ok) @@ -153,7 +165,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do let(:namespace_id) { project_namespace.id } it 'returns not-found' do - get api("/namespaces/#{namespace_id}", request_actor) + get api("#{path}/#{namespace_id}", request_actor) expect(response).to have_gitlab_http_status(:not_found) end @@ -188,7 +200,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do context "when namespace doesn't exist" do it 'returns not-found' do - get api('/namespaces/0', request_actor) + get api("#{path}/0", request_actor) expect(response).to have_gitlab_http_status(:not_found) end @@ -197,13 +209,13 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do context 'when unauthenticated' do it 'returns authentication error' do - get api("/namespaces/#{group1.id}") + get api("#{path}/#{group1.id}") expect(response).to have_gitlab_http_status(:unauthorized) end it 'returns authentication error' do - get api("/namespaces/#{project_namespace.id}") + get api("#{path}/#{project_namespace.id}") expect(response).to have_gitlab_http_status(:unauthorized) end @@ -215,7 +227,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do context 'when requested namespace is not owned by user' do context 'when requesting group' do it 'returns not-found' do - get api("/namespaces/#{group2.id}", request_actor) + get api("#{path}/#{group2.id}", request_actor) expect(response).to have_gitlab_http_status(:not_found) end @@ -223,7 +235,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do context 'when requesting personal namespace' do it 'returns not-found' do - get api("/namespaces/#{user2.namespace.id}", request_actor) + get api("#{path}/#{user2.namespace.id}", request_actor) expect(response).to have_gitlab_http_status(:not_found) end @@ -243,14 +255,14 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do let(:namespace_id) { group2.id } let(:requested_namespace) { group2 } - it_behaves_like 'can access namespace' + it_behaves_like 'can access namespace', admin_mode: true end context 'when requesting personal namespace' do let(:namespace_id) { user2.namespace.id } let(:requested_namespace) { user2.namespace } - it_behaves_like 'can access namespace' + it_behaves_like 'can access namespace', admin_mode: true end end @@ -269,7 +281,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do context 'when unauthenticated' do it 'returns authentication error' do - get api("/namespaces/#{namespace1.path}/exists") + get api("#{path}/#{namespace1.path}/exists") expect(response).to have_gitlab_http_status(:unauthorized) end @@ -278,7 +290,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do let(:namespace_id) { project_namespace.id } it 'returns authentication error' do - get api("/namespaces/#{project_namespace.path}/exists"), params: { parent_id: group2.id } + get api("#{path}/#{project_namespace.path}/exists"), params: { parent_id: group2.id } expect(response).to have_gitlab_http_status(:unauthorized) end @@ -290,12 +302,12 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do let(:current_user) { user } def request - get api("/namespaces/#{namespace1.path}/exists", current_user) + get api("#{path}/#{namespace1.path}/exists", current_user) end end it 'returns JSON indicating the namespace exists and a suggestion' do - get api("/namespaces/#{namespace1.path}/exists", user) + get api("#{path}/#{namespace1.path}/exists", user) expected_json = { exists: true, suggests: ["#{namespace1.path}1"] }.to_json expect(response).to have_gitlab_http_status(:ok) @@ -303,7 +315,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do end it 'supports dot in namespace path' do - get api("/namespaces/#{namespace_with_dot.path}/exists", user) + get api("#{path}/#{namespace_with_dot.path}/exists", user) expected_json = { exists: true, suggests: ["#{namespace_with_dot.path}1"] }.to_json expect(response).to have_gitlab_http_status(:ok) @@ -311,7 +323,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do end it 'returns JSON indicating the namespace does not exist without a suggestion' do - get api("/namespaces/non-existing-namespace/exists", user) + get api("#{path}/non-existing-namespace/exists", user) expected_json = { exists: false, suggests: [] }.to_json expect(response).to have_gitlab_http_status(:ok) @@ -319,7 +331,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do end it 'checks the existence of a namespace in case-insensitive manner' do - get api("/namespaces/#{namespace1.path.upcase}/exists", user) + get api("#{path}/#{namespace1.path.upcase}/exists", user) expected_json = { exists: true, suggests: ["#{namespace1.path.upcase}1"] }.to_json expect(response).to have_gitlab_http_status(:ok) @@ -327,7 +339,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do end it 'checks the existence within the parent namespace only' do - get api("/namespaces/#{namespace1sub.path}/exists", user), params: { parent_id: namespace1.id } + get api("#{path}/#{namespace1sub.path}/exists", user), params: { parent_id: namespace1.id } expected_json = { exists: true, suggests: ["#{namespace1sub.path}1"] }.to_json expect(response).to have_gitlab_http_status(:ok) @@ -335,7 +347,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do end it 'ignores nested namespaces when checking for top-level namespace' do - get api("/namespaces/#{namespace1sub.path}/exists", user) + get api("#{path}/#{namespace1sub.path}/exists", user) expected_json = { exists: false, suggests: [] }.to_json expect(response).to have_gitlab_http_status(:ok) @@ -349,7 +361,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do create(:group, name: 'mygroup', path: 'mygroup', parent: namespace1) - get api("/namespaces/mygroup/exists", user), params: { parent_id: namespace1.id } + get api("#{path}/mygroup/exists", user), params: { parent_id: namespace1.id } # if the paths of groups present in hierachies aren't ignored, the suggestion generated would have # been `mygroup3`, just because groups with path `mygroup1` and `mygroup2` exists somewhere else. @@ -361,7 +373,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do end it 'ignores top-level namespaces when checking with parent_id' do - get api("/namespaces/#{namespace1.path}/exists", user), params: { parent_id: namespace1.id } + get api("#{path}/#{namespace1.path}/exists", user), params: { parent_id: namespace1.id } expected_json = { exists: false, suggests: [] }.to_json expect(response).to have_gitlab_http_status(:ok) @@ -369,7 +381,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do end it 'ignores namespaces of other parent namespaces when checking with parent_id' do - get api("/namespaces/#{namespace2sub.path}/exists", user), params: { parent_id: namespace1.id } + get api("#{path}/#{namespace2sub.path}/exists", user), params: { parent_id: namespace1.id } expected_json = { exists: false, suggests: [] }.to_json expect(response).to have_gitlab_http_status(:ok) @@ -380,7 +392,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do let(:namespace_id) { project_namespace.id } it 'returns JSON indicating the namespace does not exist without a suggestion' do - get api("/namespaces/#{project_namespace.path}/exists", user), params: { parent_id: group2.id } + get api("#{path}/#{project_namespace.path}/exists", user), params: { parent_id: group2.id } expected_json = { exists: false, suggests: [] }.to_json expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index c0276e02eb7..d535629ea0d 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -70,7 +70,7 @@ RSpec.describe API::Notes, feature_category: :team_planning do describe "GET /projects/:id/noteable/:noteable_id/notes" do context "current user cannot view the notes" do - it "returns an empty array" do + it "returns an empty array", :aggregate_failures do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", user) expect(response).to have_gitlab_http_status(:ok) @@ -93,7 +93,7 @@ RSpec.describe API::Notes, feature_category: :team_planning do end context "current user can view the note" do - it "returns a non-empty array" do + it "returns a non-empty array", :aggregate_failures do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", private_user) expect(response).to have_gitlab_http_status(:ok) @@ -114,7 +114,7 @@ RSpec.describe API::Notes, feature_category: :team_planning do let(:test_url) { "/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes" } shared_examples 'a notes request' do - it 'is a note array response' do + it 'is a note array response', :aggregate_failures do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array @@ -164,7 +164,7 @@ RSpec.describe API::Notes, feature_category: :team_planning do it_behaves_like 'a notes request' - it "properly filters the returned notables" do + it "properly filters the returned notables", :aggregate_failures do expect(json_response.count).to eq(count) expect(json_response.first["system"]).to be system_notable end @@ -195,7 +195,7 @@ RSpec.describe API::Notes, feature_category: :team_planning do end context "current user can view the note" do - it "returns an issue note by id" do + it "returns an issue note by id", :aggregate_failures do get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes/#{cross_reference_note.id}", private_user) expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/requests/api/npm_instance_packages_spec.rb b/spec/requests/api/npm_instance_packages_spec.rb index dcd2e4ae677..591a8ee68dc 100644 --- a/spec/requests/api/npm_instance_packages_spec.rb +++ b/spec/requests/api/npm_instance_packages_spec.rb @@ -11,8 +11,38 @@ RSpec.describe API::NpmInstancePackages, feature_category: :package_registry do include_context 'npm api setup' describe 'GET /api/v4/packages/npm/*package_name' do - it_behaves_like 'handling get metadata requests', scope: :instance do - let(:url) { api("/packages/npm/#{package_name}") } + let(:url) { api("/packages/npm/#{package_name}") } + + it_behaves_like 'handling get metadata requests', scope: :instance + + context 'with a duplicate package name in another project' do + subject { get(url) } + + let_it_be(:project2) { create(:project, :public, namespace: namespace) } + let_it_be(:package2) do + create(:npm_package, + project: project2, + name: "@#{group.path}/scoped_package", + version: '1.2.0') + end + + it 'includes all matching package versions in the response' do + subject + + expect(json_response['versions'].keys).to match_array([package.version, package2.version]) + end + + context 'with the feature flag disabled' do + before do + stub_feature_flags(npm_allow_packages_in_multiple_projects: false) + end + + it 'returns matching package versions from only one project' do + subject + + expect(json_response['versions'].keys).to match_array([package2.version]) + end + end end end diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb index c62c0849776..1f5ebc80824 100644 --- a/spec/requests/api/npm_project_packages_spec.rb +++ b/spec/requests/api/npm_project_packages_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do + include ExclusiveLeaseHelpers + include_context 'npm api setup' shared_examples 'accept get request on private project with access to package registry for everyone' do @@ -115,6 +117,8 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do end context 'private project' do + let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_npm_user' } } + before do project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) end @@ -143,6 +147,8 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do end context 'internal project' do + let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_npm_user' } } + before do project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) end @@ -208,6 +214,14 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do it_behaves_like 'not a package tracking event' end end + + context 'invalid package attachment data' do + let(:package_name) { "@#{group.path}/my_package_name" } + let(:params) { upload_params(package_name: package_name, file: 'npm/payload_with_empty_attachment.json') } + + it_behaves_like 'handling invalid record with 400 error' + it_behaves_like 'not a package tracking event' + end end context 'valid package params' do @@ -220,15 +234,7 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do context 'with access token' do it_behaves_like 'a package tracking event', 'API::NpmPackages', 'push_package' - it 'creates npm package with file' do - expect { subject } - .to change { project.packages.count }.by(1) - .and change { Packages::PackageFile.count }.by(1) - .and change { Packages::Tag.count }.by(1) - .and change { Packages::Npm::Metadatum.count }.by(1) - - expect(response).to have_gitlab_http_status(:ok) - end + it_behaves_like 'a successful package creation' end it 'creates npm package with file with job token' do @@ -364,12 +370,13 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do end end - context 'with a too large metadata structure' do - let(:package_name) { "@#{group.path}/my_package_name" } - let(:params) do - upload_params(package_name: package_name, package_version: '1.2.3').tap do |h| - h['versions']['1.2.3']['test'] = 'test' * 10000 - end + context 'when the lease to create a package is already taken' do + let(:version) { '1.0.1' } + let(:params) { upload_params(package_name: package_name, package_version: version) } + let(:lease_key) { "packages:npm:create_package_service:packages:#{project.id}_#{package_name}_#{version}" } + + before do + stub_exclusive_lease_taken(lease_key, timeout: Packages::Npm::CreatePackageService::DEFAULT_LEASE_TIMEOUT) end it_behaves_like 'not a package tracking event' @@ -379,7 +386,95 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do .not_to change { project.packages.count } expect(response).to have_gitlab_http_status(:bad_request) - expect(response.body).to include('Validation failed: Package json structure is too large') + expect(response.body).to include('Could not obtain package lease.') + end + end + + context 'with a too large metadata structure' do + let(:package_name) { "@#{group.path}/my_package_name" } + + ::Packages::Npm::CreatePackageService::PACKAGE_JSON_NOT_ALLOWED_FIELDS.each do |field| + context "when a large value for #{field} is set" do + let(:params) do + upload_params(package_name: package_name, package_version: '1.2.3').tap do |h| + h['versions']['1.2.3'][field] = 'test' * 10000 + end + end + + it_behaves_like 'a successful package creation' + end + end + + context 'when the large field is not one of the ignored fields' do + let(:params) do + upload_params(package_name: package_name, package_version: '1.2.3').tap do |h| + h['versions']['1.2.3']['test'] = 'test' * 10000 + end + end + + it_behaves_like 'not a package tracking event' + + it 'returns an error' do + expect { upload_package_with_token } + .not_to change { project.packages.count } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(response.body).to include('Validation failed: Package json structure is too large') + end + end + end + + context 'when the Npm-Command in headers is deprecate' do + let(:package_name) { "@#{group.path}/my_package_name" } + let(:headers) { build_token_auth_header(token.plaintext_token).merge('Npm-Command' => 'deprecate') } + let(:params) do + { + 'id' => project.id.to_s, + 'package_name' => package_name, + 'versions' => { + '1.0.1' => { + 'name' => package_name, + 'deprecated' => 'This version is deprecated' + }, + '1.0.2' => { + 'name' => package_name + } + } + } + end + + subject(:request) { put api("/projects/#{project.id}/packages/npm/#{package_name.sub('/', '%2f')}"), params: params, headers: headers } + + context 'when the user is not authorized to destroy the package' do + before do + project.add_developer(user) + end + + it 'does not call DeprecatePackageService' do + expect(::Packages::Npm::DeprecatePackageService).not_to receive(:new) + + request + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when the user is authorized to destroy the package' do + before do + project.add_maintainer(user) + end + + it 'calls DeprecatePackageService with the correct arguments' do + expect(::Packages::Npm::DeprecatePackageService).to receive(:new).with(project, params) do + double.tap do |service| + expect(service).to receive(:execute).with(async: true) + end + end + + request + + expect(response).to have_gitlab_http_status(:ok) + end end end end diff --git a/spec/requests/api/nuget_group_packages_spec.rb b/spec/requests/api/nuget_group_packages_spec.rb index 4335ad75ab6..facbc01220d 100644 --- a/spec/requests/api/nuget_group_packages_spec.rb +++ b/spec/requests/api/nuget_group_packages_spec.rb @@ -12,8 +12,17 @@ RSpec.describe API::NugetGroupPackages, feature_category: :package_registry do let_it_be(:deploy_token) { create(:deploy_token, :group, read_package_registry: true, write_package_registry: true) } let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token, group: group) } - let(:snowplow_gitlab_standard_context) { { namespace: project.group, property: 'i_package_nuget_user' } } let(:target_type) { 'groups' } + let(:snowplow_gitlab_standard_context) { snowplow_context } + let(:target) { subgroup } + + def snowplow_context(user_role: :developer) + if user_role == :anonymous + { namespace: target, property: 'i_package_nuget_user' } + else + { namespace: target, property: 'i_package_nuget_user', user: user } + end + end shared_examples 'handling all endpoints' do describe 'GET /api/v4/groups/:id/-/packages/nuget' do @@ -84,7 +93,6 @@ RSpec.describe API::NugetGroupPackages, feature_category: :package_registry do context 'a group' do let(:target) { group } - let(:snowplow_gitlab_standard_context) { { namespace: target, property: 'i_package_nuget_user' } } it_behaves_like 'handling all endpoints' diff --git a/spec/requests/api/nuget_project_packages_spec.rb b/spec/requests/api/nuget_project_packages_spec.rb index 1e0d35ad451..887dfd4beeb 100644 --- a/spec/requests/api/nuget_project_packages_spec.rb +++ b/spec/requests/api/nuget_project_packages_spec.rb @@ -13,7 +13,15 @@ RSpec.describe API::NugetProjectPackages, feature_category: :package_registry do let(:target) { project } let(:target_type) { 'projects' } - let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_nuget_user' } } + let(:snowplow_gitlab_standard_context) { snowplow_context } + + def snowplow_context(user_role: :developer) + if user_role == :anonymous + { project: target, namespace: target.namespace, property: 'i_package_nuget_user' } + else + { project: target, namespace: target.namespace, property: 'i_package_nuget_user', user: user } + end + end shared_examples 'accept get request on private project with access to package registry for everyone' do subject { get api(url) } @@ -149,6 +157,7 @@ RSpec.describe API::NugetProjectPackages, feature_category: :package_registry do with_them do let(:token) { user_token ? personal_access_token.token : 'wrong' } let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) } subject { get api(url), headers: headers } diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb index b29f1e9e661..19a943477d2 100644 --- a/spec/requests/api/oauth_tokens_spec.rb +++ b/spec/requests/api/oauth_tokens_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'OAuth tokens', feature_category: :authentication_and_authorization do +RSpec.describe 'OAuth tokens', feature_category: :system_access do include HttpBasicAuthHelpers context 'Resource Owner Password Credentials' do @@ -124,6 +124,8 @@ RSpec.describe 'OAuth tokens', feature_category: :authentication_and_authorizati context 'when user account is not confirmed' do before do + stub_application_setting_enum('email_confirmation_setting', 'soft') + user.update!(confirmed_at: nil) request_oauth_token(user, client_basic_auth_header(client)) diff --git a/spec/requests/api/package_files_spec.rb b/spec/requests/api/package_files_spec.rb index f47dca387ef..3b4bd2f3cf4 100644 --- a/spec/requests/api/package_files_spec.rb +++ b/spec/requests/api/package_files_spec.rb @@ -10,6 +10,15 @@ RSpec.describe API::PackageFiles, feature_category: :package_registry do describe 'GET /projects/:id/packages/:package_id/package_files' do let(:url) { "/projects/#{project.id}/packages/#{package.id}/package_files" } + shared_examples 'handling job token and returning' do |status:| + it "returns status #{status}" do + get api(url, job_token: job.token) + + expect(response).to have_gitlab_http_status(status) + expect(response).to match_response_schema('public_api/v4/packages/package_files') if status == :ok + end + end + before do project.add_developer(user) end @@ -27,6 +36,12 @@ RSpec.describe API::PackageFiles, feature_category: :package_registry do expect(response).to have_gitlab_http_status(:not_found) end + + context 'with JOB-TOKEN auth' do + let(:job) { create(:ci_build, :running, user: user, project: project) } + + it_behaves_like 'handling job token and returning', status: :ok + end end context 'project is private' do @@ -52,6 +67,28 @@ RSpec.describe API::PackageFiles, feature_category: :package_registry do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('public_api/v4/packages/package_files') end + + context 'with JOB-TOKEN auth' do + let(:job) { create(:ci_build, :running, user: user, project: project) } + + context 'a non authenticated user' do + let(:user) { nil } + + it_behaves_like 'handling job token and returning', status: :not_found + end + + context 'a user without access to the project', :sidekiq_inline do + before do + project.team.truncate + end + + it_behaves_like 'handling job token and returning', status: :not_found + end + + context 'a user with access to the project' do + it_behaves_like 'handling job token and returning', status: :ok + end + end end context 'with pagination params' do @@ -97,6 +134,18 @@ RSpec.describe API::PackageFiles, feature_category: :package_registry do subject(:api_request) { delete api(url, user) } + shared_examples 'handling job token and returning' do |status:| + it "returns status #{status}", :aggregate_failures do + if status == :no_content + expect { api_request }.to change { package.package_files.pending_destruction.count }.by(1) + else + expect { api_request }.not_to change { package.package_files.pending_destruction.count } + end + + expect(response).to have_gitlab_http_status(status) + end + end + context 'project is public' do context 'without user' do let(:user) { nil } @@ -108,6 +157,14 @@ RSpec.describe API::PackageFiles, feature_category: :package_registry do end end + context 'with JOB-TOKEN auth' do + subject(:api_request) { delete api(url, job_token: job.token) } + + let(:job) { create(:ci_build, :running, user: user, project: project) } + + it_behaves_like 'handling job token and returning', status: :forbidden + end + it 'returns 403 for a user without access to the project', :aggregate_failures do expect { api_request }.not_to change { package.package_files.pending_destruction.count } @@ -175,6 +232,33 @@ RSpec.describe API::PackageFiles, feature_category: :package_registry do expect(response).to have_gitlab_http_status(:not_found) end end + + context 'with JOB-TOKEN auth' do + subject(:api_request) { delete api(url, job_token: job.token) } + + let(:job) { create(:ci_build, :running, user: user, project: project) } + let_it_be_with_refind(:project) { create(:project, :private) } + + context 'a user without access to the project' do + it_behaves_like 'handling job token and returning', status: :not_found + end + + context 'a user without enough permissions' do + before do + project.add_developer(user) + end + + it_behaves_like 'handling job token and returning', status: :forbidden + end + + context 'a user with the right permissions' do + before do + project.add_maintainer(user) + end + + it_behaves_like 'handling job token and returning', status: :no_content + end + end end end end diff --git a/spec/requests/api/pages/internal_access_spec.rb b/spec/requests/api/pages/internal_access_spec.rb index fdc25ecdcd3..3e7837866ae 100644 --- a/spec/requests/api/pages/internal_access_spec.rb +++ b/spec/requests/api/pages/internal_access_spec.rb @@ -35,39 +35,39 @@ RSpec.describe "Internal Project Pages Access", feature_category: :pages do describe "GET /projects/:id/pages_access" do context 'access depends on the level' do - where(:pages_access_level, :with_user, :expected_result) do - ProjectFeature::DISABLED | "admin" | 403 - ProjectFeature::DISABLED | "owner" | 403 - ProjectFeature::DISABLED | "master" | 403 - ProjectFeature::DISABLED | "developer" | 403 - ProjectFeature::DISABLED | "reporter" | 403 - ProjectFeature::DISABLED | "guest" | 403 - ProjectFeature::DISABLED | "user" | 403 - ProjectFeature::DISABLED | nil | 404 - ProjectFeature::PUBLIC | "admin" | 200 - ProjectFeature::PUBLIC | "owner" | 200 - ProjectFeature::PUBLIC | "master" | 200 - ProjectFeature::PUBLIC | "developer" | 200 - ProjectFeature::PUBLIC | "reporter" | 200 - ProjectFeature::PUBLIC | "guest" | 200 - ProjectFeature::PUBLIC | "user" | 200 - ProjectFeature::PUBLIC | nil | 404 - ProjectFeature::ENABLED | "admin" | 200 - ProjectFeature::ENABLED | "owner" | 200 - ProjectFeature::ENABLED | "master" | 200 - ProjectFeature::ENABLED | "developer" | 200 - ProjectFeature::ENABLED | "reporter" | 200 - ProjectFeature::ENABLED | "guest" | 200 - ProjectFeature::ENABLED | "user" | 200 - ProjectFeature::ENABLED | nil | 404 - ProjectFeature::PRIVATE | "admin" | 200 - ProjectFeature::PRIVATE | "owner" | 200 - ProjectFeature::PRIVATE | "master" | 200 - ProjectFeature::PRIVATE | "developer" | 200 - ProjectFeature::PRIVATE | "reporter" | 200 - ProjectFeature::PRIVATE | "guest" | 200 - ProjectFeature::PRIVATE | "user" | 403 - ProjectFeature::PRIVATE | nil | 404 + where(:pages_access_level, :with_user, :admin_mode, :expected_result) do + ProjectFeature::DISABLED | "admin" | true | 403 + ProjectFeature::DISABLED | "owner" | false | 403 + ProjectFeature::DISABLED | "master" | false | 403 + ProjectFeature::DISABLED | "developer" | false | 403 + ProjectFeature::DISABLED | "reporter" | false | 403 + ProjectFeature::DISABLED | "guest" | false | 403 + ProjectFeature::DISABLED | "user" | false | 403 + ProjectFeature::DISABLED | nil | false | 404 + ProjectFeature::PUBLIC | "admin" | false | 200 + ProjectFeature::PUBLIC | "owner" | false | 200 + ProjectFeature::PUBLIC | "master" | false | 200 + ProjectFeature::PUBLIC | "developer" | false | 200 + ProjectFeature::PUBLIC | "reporter" | false | 200 + ProjectFeature::PUBLIC | "guest" | false | 200 + ProjectFeature::PUBLIC | "user" | false | 200 + ProjectFeature::PUBLIC | nil | false | 404 + ProjectFeature::ENABLED | "admin" | false | 200 + ProjectFeature::ENABLED | "owner" | false | 200 + ProjectFeature::ENABLED | "master" | false | 200 + ProjectFeature::ENABLED | "developer" | false | 200 + ProjectFeature::ENABLED | "reporter" | false | 200 + ProjectFeature::ENABLED | "guest" | false | 200 + ProjectFeature::ENABLED | "user" | false | 200 + ProjectFeature::ENABLED | nil | false | 404 + ProjectFeature::PRIVATE | "admin" | true | 200 + ProjectFeature::PRIVATE | "owner" | false | 200 + ProjectFeature::PRIVATE | "master" | false | 200 + ProjectFeature::PRIVATE | "developer" | false | 200 + ProjectFeature::PRIVATE | "reporter" | false | 200 + ProjectFeature::PRIVATE | "guest" | false | 200 + ProjectFeature::PRIVATE | "user" | false | 403 + ProjectFeature::PRIVATE | nil | false | 404 end with_them do @@ -77,7 +77,7 @@ RSpec.describe "Internal Project Pages Access", feature_category: :pages do it "correct return value" do if !with_user.nil? user = public_send(with_user) - get api("/projects/#{project.id}/pages_access", user) + get api("/projects/#{project.id}/pages_access", user, admin_mode: admin_mode) else get api("/projects/#{project.id}/pages_access") end diff --git a/spec/requests/api/pages/pages_spec.rb b/spec/requests/api/pages/pages_spec.rb index c426f2a433c..aa1869eaa84 100644 --- a/spec/requests/api/pages/pages_spec.rb +++ b/spec/requests/api/pages/pages_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe API::Pages, feature_category: :pages do - let_it_be(:project) { create(:project, path: 'my.project', pages_https_only: false) } + let_it_be_with_reload(:project) { create(:project, path: 'my.project', pages_https_only: false) } let_it_be(:admin) { create(:admin) } let_it_be(:user) { create(:user) } @@ -13,13 +13,23 @@ RSpec.describe API::Pages, feature_category: :pages do end describe 'DELETE /projects/:id/pages' do + let(:path) { "/projects/#{project.id}/pages" } + + it_behaves_like 'DELETE request permissions for admin mode' do + before do + allow(Gitlab.config.pages).to receive(:enabled).and_return(true) + end + + let(:succes_status_code) { :no_content } + end + context 'when Pages is disabled' do before do allow(Gitlab.config.pages).to receive(:enabled).and_return(false) end it_behaves_like '404 response' do - let(:request) { delete api("/projects/#{project.id}/pages", admin) } + let(:request) { delete api(path, admin, admin_mode: true) } end end @@ -30,13 +40,13 @@ RSpec.describe API::Pages, feature_category: :pages do context 'when Pages are deployed' do it 'returns 204' do - delete api("/projects/#{project.id}/pages", admin) + delete api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:no_content) end it 'removes the pages' do - delete api("/projects/#{project.id}/pages", admin) + delete api(path, admin, admin_mode: true) expect(project.reload.pages_metadatum.deployed?).to be(false) end @@ -48,7 +58,7 @@ RSpec.describe API::Pages, feature_category: :pages do end it 'returns 204' do - delete api("/projects/#{project.id}/pages", admin) + delete api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:no_content) end @@ -58,7 +68,7 @@ RSpec.describe API::Pages, feature_category: :pages do it 'returns 404' do id = -1 - delete api("/projects/#{id}/pages", admin) + delete api("/projects/#{id}/pages", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end diff --git a/spec/requests/api/pages/private_access_spec.rb b/spec/requests/api/pages/private_access_spec.rb index 5cc1b8f9a69..602eff73b0a 100644 --- a/spec/requests/api/pages/private_access_spec.rb +++ b/spec/requests/api/pages/private_access_spec.rb @@ -35,39 +35,39 @@ RSpec.describe "Private Project Pages Access", feature_category: :pages do describe "GET /projects/:id/pages_access" do context 'access depends on the level' do - where(:pages_access_level, :with_user, :expected_result) do - ProjectFeature::DISABLED | "admin" | 403 - ProjectFeature::DISABLED | "owner" | 403 - ProjectFeature::DISABLED | "master" | 403 - ProjectFeature::DISABLED | "developer" | 403 - ProjectFeature::DISABLED | "reporter" | 403 - ProjectFeature::DISABLED | "guest" | 403 - ProjectFeature::DISABLED | "user" | 404 - ProjectFeature::DISABLED | nil | 404 - ProjectFeature::PUBLIC | "admin" | 200 - ProjectFeature::PUBLIC | "owner" | 200 - ProjectFeature::PUBLIC | "master" | 200 - ProjectFeature::PUBLIC | "developer" | 200 - ProjectFeature::PUBLIC | "reporter" | 200 - ProjectFeature::PUBLIC | "guest" | 200 - ProjectFeature::PUBLIC | "user" | 404 - ProjectFeature::PUBLIC | nil | 404 - ProjectFeature::ENABLED | "admin" | 200 - ProjectFeature::ENABLED | "owner" | 200 - ProjectFeature::ENABLED | "master" | 200 - ProjectFeature::ENABLED | "developer" | 200 - ProjectFeature::ENABLED | "reporter" | 200 - ProjectFeature::ENABLED | "guest" | 200 - ProjectFeature::ENABLED | "user" | 404 - ProjectFeature::ENABLED | nil | 404 - ProjectFeature::PRIVATE | "admin" | 200 - ProjectFeature::PRIVATE | "owner" | 200 - ProjectFeature::PRIVATE | "master" | 200 - ProjectFeature::PRIVATE | "developer" | 200 - ProjectFeature::PRIVATE | "reporter" | 200 - ProjectFeature::PRIVATE | "guest" | 200 - ProjectFeature::PRIVATE | "user" | 404 - ProjectFeature::PRIVATE | nil | 404 + where(:pages_access_level, :with_user, :admin_mode, :expected_result) do + ProjectFeature::DISABLED | "admin" | true | 403 + ProjectFeature::DISABLED | "owner" | false | 403 + ProjectFeature::DISABLED | "master" | false | 403 + ProjectFeature::DISABLED | "developer" | false | 403 + ProjectFeature::DISABLED | "reporter" | false | 403 + ProjectFeature::DISABLED | "guest" | false | 403 + ProjectFeature::DISABLED | "user" | false | 404 + ProjectFeature::DISABLED | nil | false | 404 + ProjectFeature::PUBLIC | "admin" | true | 200 + ProjectFeature::PUBLIC | "owner" | false | 200 + ProjectFeature::PUBLIC | "master" | false | 200 + ProjectFeature::PUBLIC | "developer" | false | 200 + ProjectFeature::PUBLIC | "reporter" | false | 200 + ProjectFeature::PUBLIC | "guest" | false | 200 + ProjectFeature::PUBLIC | "user" | false | 404 + ProjectFeature::PUBLIC | nil | false | 404 + ProjectFeature::ENABLED | "admin" | true | 200 + ProjectFeature::ENABLED | "owner" | false | 200 + ProjectFeature::ENABLED | "master" | false | 200 + ProjectFeature::ENABLED | "developer" | false | 200 + ProjectFeature::ENABLED | "reporter" | false | 200 + ProjectFeature::ENABLED | "guest" | false | 200 + ProjectFeature::ENABLED | "user" | false | 404 + ProjectFeature::ENABLED | nil | false | 404 + ProjectFeature::PRIVATE | "admin" | true | 200 + ProjectFeature::PRIVATE | "owner" | false | 200 + ProjectFeature::PRIVATE | "master" | false | 200 + ProjectFeature::PRIVATE | "developer" | false | 200 + ProjectFeature::PRIVATE | "reporter" | false | 200 + ProjectFeature::PRIVATE | "guest" | false | 200 + ProjectFeature::PRIVATE | "user" | false | 404 + ProjectFeature::PRIVATE | nil | false | 404 end with_them do @@ -77,7 +77,7 @@ RSpec.describe "Private Project Pages Access", feature_category: :pages do it "correct return value" do if !with_user.nil? user = public_send(with_user) - get api("/projects/#{project.id}/pages_access", user) + get api("/projects/#{project.id}/pages_access", user, admin_mode: admin_mode) else get api("/projects/#{project.id}/pages_access") end diff --git a/spec/requests/api/pages/public_access_spec.rb b/spec/requests/api/pages/public_access_spec.rb index 1137f91f4b0..8b0ed7c59ab 100644 --- a/spec/requests/api/pages/public_access_spec.rb +++ b/spec/requests/api/pages/public_access_spec.rb @@ -35,39 +35,39 @@ RSpec.describe "Public Project Pages Access", feature_category: :pages do describe "GET /projects/:id/pages_access" do context 'access depends on the level' do - where(:pages_access_level, :with_user, :expected_result) do - ProjectFeature::DISABLED | "admin" | 403 - ProjectFeature::DISABLED | "owner" | 403 - ProjectFeature::DISABLED | "master" | 403 - ProjectFeature::DISABLED | "developer" | 403 - ProjectFeature::DISABLED | "reporter" | 403 - ProjectFeature::DISABLED | "guest" | 403 - ProjectFeature::DISABLED | "user" | 403 - ProjectFeature::DISABLED | nil | 403 - ProjectFeature::PUBLIC | "admin" | 200 - ProjectFeature::PUBLIC | "owner" | 200 - ProjectFeature::PUBLIC | "master" | 200 - ProjectFeature::PUBLIC | "developer" | 200 - ProjectFeature::PUBLIC | "reporter" | 200 - ProjectFeature::PUBLIC | "guest" | 200 - ProjectFeature::PUBLIC | "user" | 200 - ProjectFeature::PUBLIC | nil | 200 - ProjectFeature::ENABLED | "admin" | 200 - ProjectFeature::ENABLED | "owner" | 200 - ProjectFeature::ENABLED | "master" | 200 - ProjectFeature::ENABLED | "developer" | 200 - ProjectFeature::ENABLED | "reporter" | 200 - ProjectFeature::ENABLED | "guest" | 200 - ProjectFeature::ENABLED | "user" | 200 - ProjectFeature::ENABLED | nil | 200 - ProjectFeature::PRIVATE | "admin" | 200 - ProjectFeature::PRIVATE | "owner" | 200 - ProjectFeature::PRIVATE | "master" | 200 - ProjectFeature::PRIVATE | "developer" | 200 - ProjectFeature::PRIVATE | "reporter" | 200 - ProjectFeature::PRIVATE | "guest" | 200 - ProjectFeature::PRIVATE | "user" | 403 - ProjectFeature::PRIVATE | nil | 403 + where(:pages_access_level, :with_user, :admin_mode, :expected_result) do + ProjectFeature::DISABLED | "admin" | false | 403 + ProjectFeature::DISABLED | "owner" | false | 403 + ProjectFeature::DISABLED | "master" | false | 403 + ProjectFeature::DISABLED | "developer" | false | 403 + ProjectFeature::DISABLED | "reporter" | false | 403 + ProjectFeature::DISABLED | "guest" | false | 403 + ProjectFeature::DISABLED | "user" | false | 403 + ProjectFeature::DISABLED | nil | false | 403 + ProjectFeature::PUBLIC | "admin" | false | 200 + ProjectFeature::PUBLIC | "owner" | false | 200 + ProjectFeature::PUBLIC | "master" | false | 200 + ProjectFeature::PUBLIC | "developer" | false | 200 + ProjectFeature::PUBLIC | "reporter" | false | 200 + ProjectFeature::PUBLIC | "guest" | false | 200 + ProjectFeature::PUBLIC | "user" | false | 200 + ProjectFeature::PUBLIC | nil | false | 200 + ProjectFeature::ENABLED | "admin" | false | 200 + ProjectFeature::ENABLED | "owner" | false | 200 + ProjectFeature::ENABLED | "master" | false | 200 + ProjectFeature::ENABLED | "developer" | false | 200 + ProjectFeature::ENABLED | "reporter" | false | 200 + ProjectFeature::ENABLED | "guest" | false | 200 + ProjectFeature::ENABLED | "user" | false | 200 + ProjectFeature::ENABLED | nil | false | 200 + ProjectFeature::PRIVATE | "admin" | true | 200 + ProjectFeature::PRIVATE | "owner" | false | 200 + ProjectFeature::PRIVATE | "master" | false | 200 + ProjectFeature::PRIVATE | "developer" | false | 200 + ProjectFeature::PRIVATE | "reporter" | false | 200 + ProjectFeature::PRIVATE | "guest" | false | 200 + ProjectFeature::PRIVATE | "user" | false | 403 + ProjectFeature::PRIVATE | nil | false | 403 end with_them do @@ -77,7 +77,7 @@ RSpec.describe "Public Project Pages Access", feature_category: :pages do it "correct return value" do if !with_user.nil? user = public_send(with_user) - get api("/projects/#{project.id}/pages_access", user) + get api("/projects/#{project.id}/pages_access", user, admin_mode: admin_mode) else get api("/projects/#{project.id}/pages_access") end diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb index ba1fb5105b8..9ca027c2edc 100644 --- a/spec/requests/api/pages_domains_spec.rb +++ b/spec/requests/api/pages_domains_spec.rb @@ -35,20 +35,24 @@ RSpec.describe API::PagesDomains, feature_category: :pages do end describe 'GET /pages/domains' do + it_behaves_like 'GET request permissions for admin mode' do + let(:path) { '/pages/domains' } + end + context 'when pages is disabled' do before do allow(Gitlab.config.pages).to receive(:enabled).and_return(false) end it_behaves_like '404 response' do - let(:request) { get api('/pages/domains', admin) } + let(:request) { get api('/pages/domains', admin, admin_mode: true) } end end context 'when pages is enabled' do context 'when authenticated as an admin' do - it 'returns paginated all pages domains' do - get api('/pages/domains', admin) + it 'returns paginated all pages domains', :aggregate_failures do + get api('/pages/domains', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('public_api/v4/pages_domain_basics') @@ -74,7 +78,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do describe 'GET /projects/:project_id/pages/domains' do shared_examples_for 'get pages domains' do - it 'returns paginated pages domains' do + it 'returns paginated pages domains', :aggregate_failures do get api(route, user) expect(response).to have_gitlab_http_status(:ok) @@ -145,7 +149,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do describe 'GET /projects/:project_id/pages/domains/:domain' do shared_examples_for 'get pages domain' do - it 'returns pages domain' do + it 'returns pages domain', :aggregate_failures do get api(route_domain, user) expect(response).to have_gitlab_http_status(:ok) @@ -155,7 +159,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do expect(json_response['certificate']).to be_nil end - it 'returns pages domain with project path' do + it 'returns pages domain with project path', :aggregate_failures do get api(route_domain_path, user) expect(response).to have_gitlab_http_status(:ok) @@ -165,7 +169,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do expect(json_response['certificate']).to be_nil end - it 'returns pages domain with a certificate' do + it 'returns pages domain with a certificate', :aggregate_failures do get api(route_secure_domain, user) expect(response).to have_gitlab_http_status(:ok) @@ -177,7 +181,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do expect(json_response['auto_ssl_enabled']).to be false end - it 'returns pages domain with an expired certificate' do + it 'returns pages domain with an expired certificate', :aggregate_failures do get api(route_expired_domain, user) expect(response).to have_gitlab_http_status(:ok) @@ -185,7 +189,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do expect(json_response['certificate']['expired']).to be true end - it 'returns pages domain with letsencrypt' do + it 'returns pages domain with letsencrypt', :aggregate_failures do get api(route_letsencrypt_domain, user) expect(response).to have_gitlab_http_status(:ok) @@ -258,7 +262,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do let(:params_secure) { pages_domain_secure_params.slice(:domain, :certificate, :key) } shared_examples_for 'post pages domains' do - it 'creates a new pages domain' do + it 'creates a new pages domain', :aggregate_failures do expect { post api(route, user), params: params } .to publish_event(PagesDomains::PagesDomainCreatedEvent) .with( @@ -279,7 +283,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do expect(pages_domain.auto_ssl_enabled).to be false end - it 'creates a new secure pages domain' do + it 'creates a new secure pages domain', :aggregate_failures do post api(route, user), params: params_secure pages_domain = PagesDomain.find_by(domain: json_response['domain']) @@ -291,7 +295,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do expect(pages_domain.auto_ssl_enabled).to be false end - it 'creates domain with letsencrypt enabled' do + it 'creates domain with letsencrypt enabled', :aggregate_failures do post api(route, user), params: pages_domain_with_letsencrypt_params pages_domain = PagesDomain.find_by(domain: json_response['domain']) @@ -301,7 +305,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do expect(pages_domain.auto_ssl_enabled).to be true end - it 'creates domain with letsencrypt enabled and provided certificate' do + it 'creates domain with letsencrypt enabled and provided certificate', :aggregate_failures do post api(route, user), params: params_secure.merge(auto_ssl_enabled: true) pages_domain = PagesDomain.find_by(domain: json_response['domain']) @@ -376,7 +380,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do let(:params_secure_nokey) { pages_domain_secure_params.slice(:certificate) } shared_examples_for 'put pages domain' do - it 'updates pages domain removing certificate' do + it 'updates pages domain removing certificate', :aggregate_failures do put api(route_secure_domain, user), params: { certificate: nil, key: nil } pages_domain_secure.reload @@ -399,7 +403,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do ) end - it 'updates pages domain adding certificate' do + it 'updates pages domain adding certificate', :aggregate_failures do put api(route_domain, user), params: params_secure pages_domain.reload @@ -409,7 +413,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do expect(pages_domain.key).to eq(params_secure[:key]) end - it 'updates pages domain adding certificate with letsencrypt' do + it 'updates pages domain adding certificate with letsencrypt', :aggregate_failures do put api(route_domain, user), params: params_secure.merge(auto_ssl_enabled: true) pages_domain.reload @@ -420,7 +424,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do expect(pages_domain.auto_ssl_enabled).to be true end - it 'updates pages domain enabling letsencrypt' do + it 'updates pages domain enabling letsencrypt', :aggregate_failures do put api(route_domain, user), params: { auto_ssl_enabled: true } pages_domain.reload @@ -429,7 +433,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do expect(pages_domain.auto_ssl_enabled).to be true end - it 'updates pages domain disabling letsencrypt while preserving the certificate' do + it 'updates pages domain disabling letsencrypt while preserving the certificate', :aggregate_failures do put api(route_letsencrypt_domain, user), params: { auto_ssl_enabled: false } pages_domain_with_letsencrypt.reload @@ -440,7 +444,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do expect(pages_domain_with_letsencrypt.certificate).to be end - it 'updates pages domain with expired certificate' do + it 'updates pages domain with expired certificate', :aggregate_failures do put api(route_expired_domain, user), params: params_secure pages_domain_expired.reload @@ -450,7 +454,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do expect(pages_domain_expired.key).to eq(params_secure[:key]) end - it 'updates pages domain with expired certificate not updating key' do + it 'updates pages domain with expired certificate not updating key', :aggregate_failures do put api(route_secure_domain, user), params: params_secure_nokey pages_domain_secure.reload diff --git a/spec/requests/api/personal_access_tokens/self_information_spec.rb b/spec/requests/api/personal_access_tokens/self_information_spec.rb index 4a3c0ad8904..3cfaaaf7d3f 100644 --- a/spec/requests/api/personal_access_tokens/self_information_spec.rb +++ b/spec/requests/api/personal_access_tokens/self_information_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::PersonalAccessTokens::SelfInformation, feature_category: :authentication_and_authorization do +RSpec.describe API::PersonalAccessTokens::SelfInformation, feature_category: :system_access do let(:path) { '/personal_access_tokens/self' } let(:token) { create(:personal_access_token, user: current_user) } @@ -12,7 +12,7 @@ RSpec.describe API::PersonalAccessTokens::SelfInformation, feature_category: :au subject(:delete_token) { delete api(path, personal_access_token: token) } shared_examples 'revoking token succeeds' do - it 'revokes token' do + it 'revokes token', :aggregate_failures do delete_token expect(response).to have_gitlab_http_status(:no_content) @@ -72,7 +72,7 @@ RSpec.describe API::PersonalAccessTokens::SelfInformation, feature_category: :au context "with a '#{scope}' scoped token" do let(:token) { create(:personal_access_token, scopes: [scope], user: current_user) } - it 'shows token info' do + it 'shows token info', :aggregate_failures do get api(path, personal_access_token: token) expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/requests/api/personal_access_tokens_spec.rb b/spec/requests/api/personal_access_tokens_spec.rb index 32adc7ebd61..166768ea605 100644 --- a/spec/requests/api/personal_access_tokens_spec.rb +++ b/spec/requests/api/personal_access_tokens_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::PersonalAccessTokens, feature_category: :authentication_and_authorization do +RSpec.describe API::PersonalAccessTokens, :aggregate_failures, feature_category: :system_access do let_it_be(:path) { '/personal_access_tokens' } describe 'GET /personal_access_tokens' do @@ -30,9 +30,13 @@ RSpec.describe API::PersonalAccessTokens, feature_category: :authentication_and_ end end + # Since all user types pass the same test successfully, we can avoid using + # shared examples and test each user type separately for its expected + # returned value. + context 'logged in as an Administrator' do let_it_be(:current_user) { create(:admin) } - let_it_be(:current_users_token) { create(:personal_access_token, user: current_user) } + let_it_be(:current_users_token) { create(:personal_access_token, :admin_mode, user: current_user) } it 'returns all PATs by default' do get api(path, current_user) @@ -46,7 +50,7 @@ RSpec.describe API::PersonalAccessTokens, feature_category: :authentication_and_ let_it_be(:token_impersonated) { create(:personal_access_token, impersonation: true, user: token.user) } it 'returns only PATs belonging to that user' do - get api(path, current_user), params: { user_id: token.user.id } + get api(path, current_user, admin_mode: true), params: { user_id: token.user.id } expect(response).to have_gitlab_http_status(:ok) expect(json_response.count).to eq(2) @@ -444,6 +448,68 @@ RSpec.describe API::PersonalAccessTokens, feature_category: :authentication_and_ end end + describe 'POST /personal_access_tokens/:id/rotate' do + let_it_be(:token) { create(:personal_access_token) } + + let(:path) { "/personal_access_tokens/#{token.id}/rotate" } + + it "rotates user's own token", :freeze_time do + post api(path, token.user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['token']).not_to eq(token.token) + expect(json_response['expires_at']).to eq((Date.today + 1.week).to_s) + end + + context 'without permission' do + it 'returns an error message' do + another_user = create(:user) + post api(path, another_user) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'when service raises an error' do + let(:error_message) { 'boom!' } + + before do + allow_next_instance_of(PersonalAccessTokens::RotateService) do |service| + allow(service).to receive(:execute).and_return(ServiceResponse.error(message: error_message)) + end + end + + it 'returns the same error message' do + post api(path, token.user) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq("400 Bad request - #{error_message}") + end + end + + context 'when token does not exist' do + let(:invalid_path) { "/personal_access_tokens/#{non_existing_record_id}/rotate" } + + context 'for non-admin user' do + it 'returns unauthorized' do + user = create(:user) + post api(invalid_path, user) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'for admin user', :enable_admin_mode do + it 'returns not found' do + admin = create(:admin) + post api(invalid_path, admin) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end + describe 'DELETE /personal_access_tokens/:id' do let_it_be(:current_user) { create(:user) } let_it_be(:token1) { create(:personal_access_token) } diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml index 60406f380a5..e9581265bb0 100644 --- a/spec/requests/api/project_attributes.yml +++ b/spec/requests/api/project_attributes.yml @@ -42,7 +42,6 @@ itself: # project - runners_token_encrypted - storage_version - topic_list - - updated_at - mirror_branch_regex remapped_attributes: avatar: avatar_url @@ -75,6 +74,7 @@ itself: # project - tag_list - topics - web_url + - description_html build_auto_devops: # auto_devops unexposed_attributes: @@ -99,7 +99,6 @@ ci_cd_settings: forward_deployment_enabled: ci_forward_deployment_enabled job_token_scope_enabled: ci_job_token_scope_enabled separated_caches: ci_separated_caches - opt_in_jwt: ci_opt_in_jwt allow_fork_pipelines_to_run_in_parent_project: ci_allow_fork_pipelines_to_run_in_parent_project build_import_state: # import_state @@ -127,6 +126,7 @@ project_feature: - package_registry_access_level - project_id - updated_at + - operations_access_level computed_attributes: - issues_enabled - jobs_enabled @@ -164,6 +164,22 @@ project_setting: - emails_enabled - pages_unique_domain_enabled - pages_unique_domain + - runner_registration_enabled + - product_analytics_instrumentation_key + - jitsu_host + - jitsu_project_xid + - jitsu_administrator_email + - jitsu_administrator_password + - encrypted_jitsu_administrator_password + - encrypted_jitsu_administrator_password_iv + - product_analytics_data_collector_host + - product_analytics_clickhouse_connection_string + - encrypted_product_analytics_clickhouse_connection_string + - encrypted_product_analytics_clickhouse_connection_string_iv + - cube_api_base_url + - cube_api_key + - encrypted_cube_api_key + - encrypted_cube_api_key_iv build_service_desk_setting: # service_desk_setting unexposed_attributes: diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb index 895192252da..c52948a4cb0 100644 --- a/spec/requests/api/project_clusters_spec.rb +++ b/spec/requests/api/project_clusters_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::ProjectClusters, feature_category: :kubernetes_management do +RSpec.describe API::ProjectClusters, feature_category: :deployment_management do include KubernetesHelpers let_it_be(:maintainer_user) { create(:user) } diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb index 096f0b73b4c..22d7ea36f6c 100644 --- a/spec/requests/api/project_export_spec.rb +++ b/spec/requests/api/project_export_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: :importers do +RSpec.describe API::ProjectExport, :aggregate_failures, :clean_gitlab_redis_cache, feature_category: :importers do let_it_be(:project) { create(:project) } let_it_be(:project_none) { create(:project) } let_it_be(:project_started) { create(:project) } @@ -45,21 +45,27 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: end describe 'GET /projects/:project_id/export' do + it_behaves_like 'GET request permissions for admin mode' do + let(:failed_status_code) { :not_found } + end + shared_examples_for 'get project export status not found' do it_behaves_like '404 response' do - let(:request) { get api(path, user) } + subject(:request) { get api(path, user) } end end shared_examples_for 'get project export status denied' do it_behaves_like '403 response' do - let(:request) { get api(path, user) } + subject(:request) { get api(path, user) } end end shared_examples_for 'get project export status ok' do + let_it_be(:admin_mode) { false } + it 'is none' do - get api(path_none, user) + get api(path_none, user, admin_mode: admin_mode) expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('public_api/v4/project/export_status') @@ -72,7 +78,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: end it 'returns status started' do - get api(path_started, user) + get api(path_started, user, admin_mode: admin_mode) expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('public_api/v4/project/export_status') @@ -82,7 +88,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: context 'when project export has finished' do it 'returns status finished' do - get api(path_finished, user) + get api(path_finished, user, admin_mode: admin_mode) expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('public_api/v4/project/export_status') @@ -96,7 +102,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: end it 'returns status regeneration_in_progress' do - get api(path_finished, user) + get api(path_finished, user, admin_mode: admin_mode) expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('public_api/v4/project/export_status') @@ -106,14 +112,16 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: end it_behaves_like 'when project export is disabled' do - let(:request) { get api(path, admin) } + subject(:request) { get api(path, admin, admin_mode: true) } end context 'when project export is enabled' do context 'when user is an admin' do let(:user) { admin } - it_behaves_like 'get project export status ok' + it_behaves_like 'get project export status ok' do + let(:admin_mode) { true } + end end context 'when user is a maintainer' do @@ -159,29 +167,34 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: end describe 'GET /projects/:project_id/export/download' do + it_behaves_like 'GET request permissions for admin mode' do + let(:path) { download_path_finished } + let(:failed_status_code) { :not_found } + end + shared_examples_for 'get project export download not found' do it_behaves_like '404 response' do - let(:request) { get api(download_path, user) } + subject(:request) { get api(download_path, user) } end end shared_examples_for 'get project export download denied' do it_behaves_like '403 response' do - let(:request) { get api(download_path, user) } + subject(:request) { get api(download_path, user) } end end shared_examples_for 'get project export download' do it_behaves_like '404 response' do - let(:request) { get api(download_path_none, user) } + subject(:request) { get api(download_path_none, user, admin_mode: admin_mode) } end it_behaves_like '404 response' do - let(:request) { get api(download_path_started, user) } + subject(:request) { get api(download_path_started, user, admin_mode: admin_mode) } end it 'downloads' do - get api(download_path_finished, user) + get api(download_path_finished, user, admin_mode: admin_mode) expect(response).to have_gitlab_http_status(:ok) end @@ -190,7 +203,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: shared_examples_for 'get project export upload after action' do context 'and is uploading' do it 'downloads' do - get api(download_path_export_action, user) + get api(download_path_export_action, user, admin_mode: admin_mode) expect(response).to have_gitlab_http_status(:ok) end @@ -202,7 +215,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: end it 'returns 404' do - get api(download_path_export_action, user) + get api(download_path_export_action, user, admin_mode: admin_mode) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('The project export file is not available yet') @@ -219,12 +232,14 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: end it_behaves_like '404 response' do - let(:request) { get api(download_path_export_action, user) } + subject(:request) { get api(download_path_export_action, user, admin_mode: admin_mode) } end end end shared_examples_for 'get project download by strategy' do + let_it_be(:admin_mode) { false } + context 'when upload strategy set' do it_behaves_like 'get project export upload after action' end @@ -235,17 +250,19 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: end it_behaves_like 'when project export is disabled' do - let(:request) { get api(download_path, admin) } + subject(:request) { get api(download_path, admin, admin_mode: true) } end context 'when project export is enabled' do context 'when user is an admin' do let(:user) { admin } - it_behaves_like 'get project download by strategy' + it_behaves_like 'get project download by strategy' do + let(:admin_mode) { true } + end context 'when rate limit is exceeded' do - let(:request) { get api(download_path, admin) } + subject(:request) { get api(download_path, admin, admin_mode: true) } before do allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy| @@ -271,7 +288,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: # simulate prior request to the same namespace, which increments the rate limit counter for that scope Gitlab::ApplicationRateLimiter.throttled?(:project_download_export, scope: [user, project_finished.namespace]) - get api(download_path_finished, user) + get api(download_path_finished, user, admin_mode: true) expect(response).to have_gitlab_http_status(:too_many_requests) end @@ -280,7 +297,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: Gitlab::ApplicationRateLimiter.throttled?(:project_download_export, scope: [user, create(:project, :with_export).namespace]) - get api(download_path_finished, user) + get api(download_path_finished, user, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) end end @@ -345,30 +362,41 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: end describe 'POST /projects/:project_id/export' do + let(:admin_mode) { false } + let(:params) { {} } + + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { { 'upload[url]' => 'http://gitlab.com' } } + let(:failed_status_code) { :not_found } + let(:success_status_code) { :accepted } + end + + subject(:request) { post api(path, user, admin_mode: admin_mode), params: params } + shared_examples_for 'post project export start not found' do - it_behaves_like '404 response' do - let(:request) { post api(path, user) } - end + it_behaves_like '404 response' end shared_examples_for 'post project export start denied' do - it_behaves_like '403 response' do - let(:request) { post api(path, user) } - end + it_behaves_like '403 response' end shared_examples_for 'post project export start' do + let_it_be(:admin_mode) { false } + context 'with upload strategy' do context 'when params invalid' do it_behaves_like '400 response' do - let(:request) { post(api(path, user), params: { 'upload[url]' => 'whatever' }) } + let(:params) { { 'upload[url]' => 'whatever' } } end end it 'starts' do allow_any_instance_of(Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy).to receive(:send_file) - post(api(path, user), params: { 'upload[url]' => 'http://gitlab.com' }) + request do + let(:params) { { 'upload[url]' => 'http://gitlab.com' } } + end expect(response).to have_gitlab_http_status(:accepted) end @@ -388,7 +416,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: it 'starts' do expect_any_instance_of(Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy).not_to receive(:send_file) - post api(path, user) + request expect(response).to have_gitlab_http_status(:accepted) end @@ -396,20 +424,21 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: it 'removes previously exported archive file' do expect(project).to receive(:remove_exports).once - post api(path, user) + request end end end - it_behaves_like 'when project export is disabled' do - let(:request) { post api(path, admin) } - end + it_behaves_like 'when project export is disabled' context 'when project export is enabled' do context 'when user is an admin' do let(:user) { admin } + let(:admin_mode) { true } - it_behaves_like 'post project export start' + it_behaves_like 'post project export start' do + let(:admin_mode) { true } + end context 'with project export size limit' do before do @@ -417,7 +446,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: end it 'starts if limit not exceeded' do - post api(path, user) + request expect(response).to have_gitlab_http_status(:accepted) end @@ -425,7 +454,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: it '400 response if limit exceeded' do project.statistics.update!(lfs_objects_size: 2.megabytes, repository_size: 2.megabytes) - post api(path, user) + request expect(response).to have_gitlab_http_status(:bad_request) expect(json_response["message"]).to include('The project size exceeds the export limit.') @@ -441,7 +470,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: end it 'prevents requesting project export' do - post api(path, admin) + request 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.') @@ -559,7 +588,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: let(:relation) { ::BulkImports::FileTransfer::ProjectConfig.new(project).skipped_relations.first } it_behaves_like '400 response' do - let(:request) { get api(download_path, user) } + subject(:request) { get api(download_path, user) } end end @@ -595,7 +624,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: describe 'POST /projects/:id/export_relations' do it_behaves_like '404 response' do - let(:request) { post api(path, user) } + subject(:request) { post api(path, user) } end end @@ -608,13 +637,13 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: end it_behaves_like '404 response' do - let(:request) { post api(path, user) } + subject(: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) } + subject(:request) { get api(status_path, user) } end end end @@ -629,26 +658,26 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: describe 'POST /projects/:id/export_relations' do it_behaves_like '403 response' do - let(:request) { post api(path, developer) } + subject(: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) } + subject(: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) } + subject(:request) { get api(status_path, developer) } end end end context 'when bulk import is disabled' do it_behaves_like '404 response' do - let(:request) { get api(path, user) } + subject(:request) { get api(path, user) } end end end diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index 027c61bb9e1..4496e3aa7c3 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -14,6 +14,8 @@ RSpec.describe API::ProjectImport, :aggregate_failures, feature_category: :impor before do namespace.add_owner(user) if user + + stub_application_setting(import_sources: ['gitlab_project']) end shared_examples 'requires authentication' do @@ -26,6 +28,20 @@ RSpec.describe API::ProjectImport, :aggregate_failures, feature_category: :impor end end + shared_examples 'requires import source to be enabled' do + context 'when gitlab_project import_sources is disabled' do + before do + stub_application_setting(import_sources: []) + end + + it 'returns 403' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + describe 'POST /projects/import' do subject { upload_archive(file_upload, workhorse_headers, params) } @@ -43,6 +59,7 @@ RSpec.describe API::ProjectImport, :aggregate_failures, feature_category: :impor end it_behaves_like 'requires authentication' + it_behaves_like 'requires import source to be enabled' it 'executes a limited number of queries', :use_clean_rails_redis_caching do control_count = ActiveRecord::QueryRecorder.new { subject }.count @@ -247,7 +264,7 @@ RSpec.describe API::ProjectImport, :aggregate_failures, feature_category: :impor subject expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to eq('Project namespace name has already been taken') + expect(json_response['message']).to eq('Path has already been taken') end context 'when param overwrite is true' do @@ -337,6 +354,7 @@ RSpec.describe API::ProjectImport, :aggregate_failures, feature_category: :impor end it_behaves_like 'requires authentication' + it_behaves_like 'requires import source to be enabled' context 'when the response is successful' do it 'schedules the import successfully' do @@ -402,64 +420,51 @@ RSpec.describe API::ProjectImport, :aggregate_failures, feature_category: :impor end it_behaves_like 'requires authentication' + it_behaves_like 'requires import source to be enabled' - it 'returns NOT FOUND when the feature is disabled' do - stub_feature_flags(import_project_from_remote_file_s3: false) - - subject - - expect(response).to have_gitlab_http_status(:not_found) - end - - context 'when the feature flag is enabled' do - before do - stub_feature_flags(import_project_from_remote_file_s3: true) - end - - context 'when the response is successful' do - it 'schedules the import successfully' do - project = create( - :project, - namespace: user.namespace, - name: 'test-import', - path: 'test-import' - ) + context 'when the response is successful' do + it 'schedules the import successfully' do + project = create( + :project, + namespace: user.namespace, + name: 'test-import', + path: 'test-import' + ) - service_response = ServiceResponse.success(payload: project) - expect_next(::Import::GitlabProjects::CreateProjectService) - .to receive(:execute) - .and_return(service_response) + service_response = ServiceResponse.success(payload: project) + expect_next(::Import::GitlabProjects::CreateProjectService) + .to receive(:execute) + .and_return(service_response) - subject + subject - expect(response).to have_gitlab_http_status(:created) - expect(json_response).to include({ - 'id' => project.id, - 'name' => 'test-import', - 'name_with_namespace' => "#{user.namespace.name} / test-import", - 'path' => 'test-import', - 'path_with_namespace' => "#{user.namespace.path}/test-import" - }) - end + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to include({ + 'id' => project.id, + 'name' => 'test-import', + 'name_with_namespace' => "#{user.namespace.name} / test-import", + 'path' => 'test-import', + 'path_with_namespace' => "#{user.namespace.path}/test-import" + }) end + end - context 'when the service returns an error' do - it 'fails to schedule the import' do - service_response = ServiceResponse.error( - message: 'Failed to import', - http_status: :bad_request - ) - expect_next(::Import::GitlabProjects::CreateProjectService) - .to receive(:execute) - .and_return(service_response) + context 'when the service returns an error' do + it 'fails to schedule the import' do + service_response = ServiceResponse.error( + message: 'Failed to import', + http_status: :bad_request + ) + expect_next(::Import::GitlabProjects::CreateProjectService) + .to receive(:execute) + .and_return(service_response) - subject + subject - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response).to eq({ - 'message' => 'Failed to import' - }) - end + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response).to eq({ + 'message' => 'Failed to import' + }) end end end @@ -510,6 +515,7 @@ RSpec.describe API::ProjectImport, :aggregate_failures, feature_category: :impor subject { post api('/projects/import/authorize', user), headers: workhorse_headers } it_behaves_like 'requires authentication' + it_behaves_like 'requires import source to be enabled' it 'authorizes importing project with workhorse header' do subject diff --git a/spec/requests/api/project_job_token_scope_spec.rb b/spec/requests/api/project_job_token_scope_spec.rb new file mode 100644 index 00000000000..df210a00012 --- /dev/null +++ b/spec/requests/api/project_job_token_scope_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::ProjectJobTokenScope, feature_category: :secrets_management do + describe 'GET /projects/:id/job_token_scope' do + let_it_be(:project) { create(:project, :public) } + let_it_be(:user) { create(:user) } + + let(:get_job_token_scope_path) { "/projects/#{project.id}/job_token_scope" } + + subject { get api(get_job_token_scope_path, user) } + + context 'when unauthenticated user (missing user)' do + context 'for public project' do + it 'does not return ci cd settings of job token' do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + + get api(get_job_token_scope_path) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + context 'when authenticated user as maintainer' do + before_all { project.add_maintainer(user) } + + it 'returns ci cd settings for job token scope' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + "inbound_enabled" => true, + "outbound_enabled" => false + ) + end + + it 'returns the correct ci cd settings for job token scope after change' do + project.update!(ci_inbound_job_token_scope_enabled: false) + + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + "inbound_enabled" => false, + "outbound_enabled" => false + ) + end + + it 'returns unauthorized and blank response when invalid auth credentials are given' do + invalid_personal_access_token = build(:personal_access_token, user: user) + + get api(get_job_token_scope_path, user, personal_access_token: invalid_personal_access_token) + + expect(response).to have_gitlab_http_status(:unauthorized) + expect(json_response).not_to include("inbound_enabled", "outbound_enabled") + end + end + + context 'when authenticated user as developer' do + before do + project.add_developer(user) + end + + it 'returns forbidden and no ci cd settings for public project' do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response).not_to include("inbound_enabled", "outbound_enabled") + end + end + end +end diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb index 9d722e4a445..978ac28ef73 100644 --- a/spec/requests/api/project_milestones_spec.rb +++ b/spec/requests/api/project_milestones_spec.rb @@ -6,8 +6,12 @@ RSpec.describe API::ProjectMilestones, feature_category: :team_planning do let_it_be(:user) { create(:user) } let_it_be_with_reload(:project) { create(:project, namespace: user.namespace) } let_it_be(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') } - let_it_be(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') } let_it_be(:route) { "/projects/#{project.id}/milestones" } + let_it_be(:milestone) do + create(:milestone, project: project, title: 'version2', description: 'open milestone', updated_at: 5.days.ago) + end + + let(:params) { {} } before_all do project.add_reporter(user) @@ -15,38 +19,43 @@ RSpec.describe API::ProjectMilestones, feature_category: :team_planning do it_behaves_like 'group and project milestones', "/projects/:id/milestones" + shared_examples 'listing all milestones' do + it 'returns correct list of milestones' do + get api(route, user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(milestones.size) + expect(json_response.map { |entry| entry["id"] }).to match_array(milestones.map(&:id)) + end + end + describe 'GET /projects/:id/milestones' do - context 'when include_parent_milestones is true' do - let_it_be(:ancestor_group) { create(:group, :private) } - let_it_be(:group) { create(:group, :private, parent: ancestor_group) } - let_it_be(:ancestor_group_milestone) { create(:milestone, group: ancestor_group) } - let_it_be(:group_milestone) { create(:milestone, group: group) } + let_it_be(:ancestor_group) { create(:group, :private) } + let_it_be(:group) { create(:group, :private, parent: ancestor_group) } + let_it_be(:ancestor_group_milestone) { create(:milestone, group: ancestor_group, updated_at: 1.day.ago) } + let_it_be(:group_milestone) { create(:milestone, group: group, updated_at: 3.days.ago) } - let(:params) { { include_parent_milestones: true } } + context 'when project parent is a namespace' do + let(:milestones) { [milestone, closed_milestone] } - shared_examples 'listing all milestones' do - it 'returns correct list of milestones' do - get api(route, user), params: params + it_behaves_like 'listing all milestones' - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.size).to eq(milestones.size) - expect(json_response.map { |entry| entry["id"] }).to eq(milestones.map(&:id)) - end + context 'when include_parent_milestones is true' do + let(:params) { { include_parent_milestones: true } } + + it_behaves_like 'listing all milestones' end + end - context 'when project parent is a namespace' do - it_behaves_like 'listing all milestones' do - let(:milestones) { [milestone, closed_milestone] } - end + context 'when project parent is a group' do + before_all do + project.update!(namespace: group) end - context 'when project parent is a group' do + context 'when include_parent_milestones is true' do + let(:params) { { include_parent_milestones: true } } let(:milestones) { [group_milestone, ancestor_group_milestone, milestone, closed_milestone] } - before_all do - project.update!(namespace: group) - end - it_behaves_like 'listing all milestones' context 'when iids param is present' do @@ -64,6 +73,38 @@ RSpec.describe API::ProjectMilestones, feature_category: :team_planning do expect(response).to have_gitlab_http_status(:not_found) end end + + context 'when updated_before param is present' do + let(:params) { { updated_before: 12.hours.ago.iso8601, include_parent_milestones: true } } + + it_behaves_like 'listing all milestones' do + let(:milestones) { [group_milestone, ancestor_group_milestone, milestone] } + end + end + + context 'when updated_after param is present' do + let(:params) { { updated_after: 2.days.ago.iso8601, include_parent_milestones: true } } + + it_behaves_like 'listing all milestones' do + let(:milestones) { [ancestor_group_milestone, closed_milestone] } + end + end + end + + context 'when updated_before param is present' do + let(:params) { { updated_before: 12.hours.ago.iso8601 } } + + it_behaves_like 'listing all milestones' do + let(:milestones) { [milestone] } + end + end + + context 'when updated_after param is present' do + let(:params) { { updated_after: 2.days.ago.iso8601 } } + + it_behaves_like 'listing all milestones' do + let(:milestones) { [closed_milestone] } + end end end end diff --git a/spec/requests/api/project_snapshots_spec.rb b/spec/requests/api/project_snapshots_spec.rb index 5d3c596e605..cbf6907f9a3 100644 --- a/spec/requests/api/project_snapshots_spec.rb +++ b/spec/requests/api/project_snapshots_spec.rb @@ -2,11 +2,12 @@ require 'spec_helper' -RSpec.describe API::ProjectSnapshots, feature_category: :source_code_management do +RSpec.describe API::ProjectSnapshots, :aggregate_failures, feature_category: :source_code_management do include WorkhorseHelpers let(:project) { create(:project) } let(:admin) { create(:admin) } + let(:path) { "/projects/#{project.id}/snapshot" } before do allow(Feature::Gitaly).to receive(:server_feature_flags).and_return({ @@ -32,27 +33,29 @@ RSpec.describe API::ProjectSnapshots, feature_category: :source_code_management expect(response.parsed_body).to be_empty end + it_behaves_like 'GET request permissions for admin mode' + it 'returns authentication error as project owner' do - get api("/projects/#{project.id}/snapshot", project.first_owner) + get api(path, project.first_owner) expect(response).to have_gitlab_http_status(:forbidden) end it 'returns authentication error as unauthenticated user' do - get api("/projects/#{project.id}/snapshot", nil) + get api(path, nil) expect(response).to have_gitlab_http_status(:unauthorized) end it 'requests project repository raw archive as administrator' do - get api("/projects/#{project.id}/snapshot", admin), params: { wiki: '0' } + get api(path, admin, admin_mode: true), params: { wiki: '0' } expect(response).to have_gitlab_http_status(:ok) expect_snapshot_response_for(project.repository) end it 'requests wiki repository raw archive as administrator' do - get api("/projects/#{project.id}/snapshot", admin), params: { wiki: '1' } + get api(path, admin, admin_mode: true), params: { wiki: '1' } expect(response).to have_gitlab_http_status(:ok) expect_snapshot_response_for(project.wiki.repository) diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 267557b8137..f0aa61c688b 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::ProjectSnippets, feature_category: :source_code_management do +RSpec.describe API::ProjectSnippets, :aggregate_failures, feature_category: :source_code_management do include SnippetHelpers let_it_be(:project) { create(:project, :public) } @@ -14,8 +14,12 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d describe "GET /projects/:project_id/snippets/:id/user_agent_detail" do let_it_be(:user_agent_detail) { create(:user_agent_detail, subject: public_snippet) } + it_behaves_like 'GET request permissions for admin mode' do + let(:path) { "/projects/#{public_snippet.project.id}/snippets/#{public_snippet.id}/user_agent_detail" } + end + it 'exposes known attributes' do - get api("/projects/#{project.id}/snippets/#{public_snippet.id}/user_agent_detail", admin) + get api("/projects/#{project.id}/snippets/#{public_snippet.id}/user_agent_detail", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['user_agent']).to eq(user_agent_detail.user_agent) @@ -26,7 +30,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d it 'respects project scoping' do other_project = create(:project) - get api("/projects/#{other_project.id}/snippets/#{public_snippet.id}/user_agent_detail", admin) + get api("/projects/#{other_project.id}/snippets/#{public_snippet.id}/user_agent_detail", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end @@ -38,7 +42,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d context 'with snippets disabled' do it_behaves_like '403 response' do - let(:request) { get api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}/user_agent_detail", admin) } + subject(:request) { get api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}/user_agent_detail", admin, admin_mode: true) } end end end @@ -72,7 +76,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d context 'with snippets disabled' do it_behaves_like '403 response' do - let(:request) { get api("/projects/#{project_no_snippets.id}/snippets", user) } + subject(:request) { get api("/projects/#{project_no_snippets.id}/snippets", user) } end end end @@ -83,16 +87,14 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d it 'returns snippet json' do get api("/projects/#{project.id}/snippets/#{snippet.id}", user) - aggregate_failures do - expect(response).to have_gitlab_http_status(:ok) + expect(response).to have_gitlab_http_status(:ok) - expect(json_response['title']).to eq(snippet.title) - expect(json_response['description']).to eq(snippet.description) - expect(json_response['file_name']).to eq(snippet.file_name_on_repo) - expect(json_response['files']).to eq(snippet.blobs.map { |blob| snippet_blob_file(blob) }) - expect(json_response['ssh_url_to_repo']).to eq(snippet.ssh_url_to_repo) - expect(json_response['http_url_to_repo']).to eq(snippet.http_url_to_repo) - end + expect(json_response['title']).to eq(snippet.title) + expect(json_response['description']).to eq(snippet.description) + expect(json_response['file_name']).to eq(snippet.file_name_on_repo) + expect(json_response['files']).to eq(snippet.blobs.map { |blob| snippet_blob_file(blob) }) + expect(json_response['ssh_url_to_repo']).to eq(snippet.ssh_url_to_repo) + expect(json_response['http_url_to_repo']).to eq(snippet.http_url_to_repo) end it 'returns 404 for invalid snippet id' do @@ -104,7 +106,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d context 'with snippets disabled' do it_behaves_like '403 response' do - let(:request) { get api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}", user) } + subject(:request) { get api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}", user) } end end @@ -126,22 +128,25 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d let(:file_content) { 'puts "hello world"' } let(:file_params) { { files: [{ file_path: file_path, content: file_content }] } } let(:params) { base_params.merge(file_params) } + let(:admin_mode) { false } + + subject(:request) { post api("/projects/#{project.id}/snippets/", actor, admin_mode: admin_mode), params: params } - subject { post api("/projects/#{project.id}/snippets/", actor), params: params } + it_behaves_like 'POST request permissions for admin mode' do + let(:path) { "/projects/#{project.id}/snippets/" } + end shared_examples 'project snippet repository actions' do let(:snippet) { ProjectSnippet.find(json_response['id']) } it 'commit the files to the repository' do - subject + request - aggregate_failures do - expect(snippet.repository.exists?).to be_truthy + expect(snippet.repository.exists?).to be_truthy - blob = snippet.repository.blob_at(snippet.default_branch, file_path) + blob = snippet.repository.blob_at(snippet.default_branch, file_path) - expect(blob.data).to eq file_content - end + expect(blob.data).to eq file_content end end @@ -152,7 +157,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d it 'creates a new snippet' do project.add_developer(actor) - subject + request expect(response).to have_gitlab_http_status(:created) end @@ -160,7 +165,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d context 'that does not belong to the project' do it 'does not create a new snippet' do - subject + request expect(response).to have_gitlab_http_status(:forbidden) end @@ -180,7 +185,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d end it 'creates a new snippet' do - subject + request expect(response).to have_gitlab_http_status(:created) snippet = ProjectSnippet.find(json_response['id']) @@ -196,9 +201,10 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d context 'with an admin' do let(:actor) { admin } + let(:admin_mode) { true } it 'creates a new snippet' do - subject + request expect(response).to have_gitlab_http_status(:created) snippet = ProjectSnippet.find(json_response['id']) @@ -214,7 +220,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d it 'returns 400 for missing parameters' do params.delete(:title) - subject + request expect(response).to have_gitlab_http_status(:bad_request) end @@ -226,7 +232,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d it 'returns 400 if title is blank' do params[:title] = '' - subject + request expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq 'title is empty' @@ -235,6 +241,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d context 'when save fails because the repository could not be created' do let(:actor) { admin } + let(:admin_mode) { true } before do allow_next_instance_of(Snippets::CreateService) do |instance| @@ -243,7 +250,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d end it 'returns 400' do - subject + request expect(response).to have_gitlab_http_status(:bad_request) end @@ -264,7 +271,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d it 'creates the snippet' do params['visibility'] = 'private' - expect { subject }.to change { Snippet.count }.by(1) + expect { request }.to change { Snippet.count }.by(1) end end @@ -274,13 +281,13 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d end it 'rejects the snippet' do - expect { subject }.not_to change { Snippet.count } + expect { request }.not_to change { Snippet.count } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']['error']).to match(/snippet has been recognized as spam/) end it 'creates a spam log' do - expect { subject } + expect { request } .to log_spam(title: 'Test Title', user_id: user.id, noteable_type: 'ProjectSnippet') end end @@ -288,7 +295,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d context 'with snippets disabled' do it_behaves_like '403 response' do - let(:request) { post api("/projects/#{project_no_snippets.id}/snippets", user), params: params } + subject(:request) { post api("/projects/#{project_no_snippets.id}/snippets", user), params: params } end end end @@ -296,6 +303,11 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d describe 'PUT /projects/:project_id/snippets/:id/' do let(:visibility_level) { Snippet::PUBLIC } let(:snippet) { create(:project_snippet, :repository, author: admin, visibility_level: visibility_level, project: project) } + let(:params) { { title: 'Foo' } } + + it_behaves_like 'PUT request permissions for admin mode' do + let(:path) { "/projects/#{snippet.project.id}/snippets/#{snippet.id}" } + end it_behaves_like 'snippet file updates' it_behaves_like 'snippet non-file updates' @@ -317,7 +329,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d let(:visibility_level) { Snippet::PRIVATE } it 'creates the snippet' do - expect { update_snippet(params: { title: 'Foo' }) } + expect { update_snippet(admin_mode: true, params: params) } .to change { snippet.reload.title }.to('Foo') end end @@ -326,12 +338,12 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d let(:visibility_level) { Snippet::PUBLIC } it 'rejects the snippet' do - expect { update_snippet(params: { title: 'Foo' }) } + expect { update_snippet(params: params) } .not_to change { snippet.reload.title } end it 'creates a spam log' do - expect { update_snippet(params: { title: 'Foo' }) } + expect { update_snippet(params: params) } .to log_spam(title: 'Foo', user_id: admin.id, noteable_type: 'ProjectSnippet') end end @@ -340,7 +352,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d let(:visibility_level) { Snippet::PRIVATE } it 'rejects the snippet' do - expect { update_snippet(params: { title: 'Foo', visibility: 'public' }) } + expect { update_snippet(admin_mode: true, params: { title: 'Foo', visibility: 'public' }) } .not_to change { snippet.reload.title } expect(response).to have_gitlab_http_status(:bad_request) @@ -348,7 +360,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d end it 'creates a spam log' do - expect { update_snippet(params: { title: 'Foo', visibility: 'public' }) } + expect { update_snippet(admin_mode: true, params: { title: 'Foo', visibility: 'public' }) } .to log_spam(title: 'Foo', user_id: admin.id, noteable_type: 'ProjectSnippet') end end @@ -356,47 +368,58 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d context 'with snippets disabled' do it_behaves_like '403 response' do - let(:request) { put api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}", admin), params: { description: 'foo' } } + subject(:request) { put api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}", admin, admin_mode: true), params: { description: 'foo' } } end end - def update_snippet(snippet_id: snippet.id, params: {}) - put api("/projects/#{snippet.project.id}/snippets/#{snippet_id}", admin), params: params + def update_snippet(snippet_id: snippet.id, admin_mode: false, params: {}) + put api("/projects/#{snippet.project.id}/snippets/#{snippet_id}", admin, admin_mode: admin_mode), params: params end end describe 'DELETE /projects/:project_id/snippets/:id/' do let_it_be(:snippet, refind: true) { public_snippet } + let(:path) { "/projects/#{snippet.project.id}/snippets/#{snippet.id}/" } + + it_behaves_like 'DELETE request permissions for admin mode' it 'deletes snippet' do - delete api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin) + delete api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:no_content) end it 'returns 404 for invalid snippet id' do - delete api("/projects/#{snippet.project.id}/snippets/#{non_existing_record_id}", admin) + delete api("/projects/#{snippet.project.id}/snippets/#{non_existing_record_id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Snippet Not Found') end it_behaves_like '412 response' do - let(:request) { api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin) } + subject(:request) { api(path, admin, admin_mode: true) } end context 'with snippets disabled' do it_behaves_like '403 response' do - let(:request) { delete api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}", admin) } + subject(:request) { delete api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}", admin, admin_mode: true) } end end end describe 'GET /projects/:project_id/snippets/:id/raw' do let_it_be(:snippet) { create(:project_snippet, :repository, :public, author: admin, project: project) } + let(:path) { "/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw" } + + it_behaves_like 'GET request permissions for admin mode' do + let_it_be(:snippet_with_empty_repo) { create(:project_snippet, :empty_repo, author: admin, project: project) } + + let(:snippet) { snippet_with_empty_repo } + let(:failed_status_code) { :not_found } + end it 'returns raw text' do - get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin) + get api(path, admin) expect(response).to have_gitlab_http_status(:ok) expect(response.media_type).to eq 'text/plain' @@ -404,38 +427,41 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d end it 'returns 404 for invalid snippet id' do - get api("/projects/#{snippet.project.id}/snippets/#{non_existing_record_id}/raw", admin) + get api("/projects/#{snippet.project.id}/snippets/#{non_existing_record_id}/raw", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Snippet Not Found') end - it_behaves_like 'project snippet access levels' do - let(:path) { "/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw" } - end + it_behaves_like 'project snippet access levels' context 'with snippets disabled' do it_behaves_like '403 response' do - let(:request) { get api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}/raw", admin) } + subject(:request) { get api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}/raw", admin, admin_mode: true) } end end it_behaves_like 'snippet blob content' do let_it_be(:snippet_with_empty_repo) { create(:project_snippet, :empty_repo, author: admin, project: project) } + let_it_be(:admin_mode) { snippet.author.admin? } - subject { get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", snippet.author) } + subject { get api(path, snippet.author, admin_mode: admin_mode) } end end describe 'GET /projects/:project_id/snippets/:id/files/:ref/:file_path/raw' do let_it_be(:snippet) { create(:project_snippet, :repository, author: admin, project: project) } + let(:path) { "/projects/#{snippet.project.id}/snippets/#{snippet.id}/files/master/%2Egitattributes/raw" } + + it_behaves_like 'GET request permissions for admin mode' do + let(:failed_status_code) { :not_found } + end + it_behaves_like 'raw snippet files' do let(:api_path) { "/projects/#{snippet.project.id}/snippets/#{snippet_id}/files/#{ref}/#{file_path}/raw" } end - it_behaves_like 'project snippet access levels' do - let(:path) { "/projects/#{snippet.project.id}/snippets/#{snippet.id}/files/master/%2Egitattributes/raw" } - end + it_behaves_like 'project snippet access levels' end end diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb index 38d6a05a104..91e5ed76c37 100644 --- a/spec/requests/api/project_templates_spec.rb +++ b/spec/requests/api/project_templates_spec.rb @@ -10,6 +10,7 @@ RSpec.describe API::ProjectTemplates, feature_category: :source_code_management let(:url_encoded_path) { "#{public_project.namespace.path}%2F#{public_project.path}" } before do + stub_feature_flags(remove_monitor_metrics: false) private_project.add_developer(developer) end @@ -71,6 +72,18 @@ RSpec.describe API::ProjectTemplates, feature_category: :source_code_management expect(json_response).to satisfy_one { |template| template['key'] == 'Default' } end + context 'when metrics dashboard feature is unavailable' do + before do + stub_feature_flags(remove_monitor_metrics: true) + end + + it 'returns 400 bad request like other unknown types' do + get api("/projects/#{public_project.id}/templates/metrics_dashboard_ymls") + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + it 'returns issue templates' do get api("/projects/#{private_project.id}/templates/issues", developer) @@ -171,6 +184,18 @@ RSpec.describe API::ProjectTemplates, feature_category: :source_code_management expect(json_response['name']).to eq('Default') end + context 'when metrics dashboard feature is unavailable' do + before do + stub_feature_flags(remove_monitor_metrics: true) + end + + it 'returns 400 bad request like other unknown types' do + get api("/projects/#{public_project.id}/templates/metrics_dashboard_ymls/Default") + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + it 'returns a specific license' do get api("/projects/#{public_project.id}/templates/licenses/mit") diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index e78ef2f7630..349101a092f 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -15,7 +15,7 @@ RSpec.shared_examples 'languages and percentages JSON response' do end context "when the languages haven't been detected yet" do - it 'returns expected language values', :sidekiq_might_not_need_inline do + it 'returns expected language values', :aggregate_failures, :sidekiq_might_not_need_inline do get api("/projects/#{project.id}/languages", user) expect(response).to have_gitlab_http_status(:ok) @@ -33,7 +33,7 @@ RSpec.shared_examples 'languages and percentages JSON response' do Projects::DetectRepositoryLanguagesService.new(project, project.first_owner).execute end - it 'returns the detection from the database' do + it 'returns the detection from the database', :aggregate_failures do # Allow this to happen once, so the expected languages can be determined expect(project.repository).to receive(:languages).once @@ -46,7 +46,7 @@ RSpec.shared_examples 'languages and percentages JSON response' do end end -RSpec.describe API::Projects, feature_category: :projects do +RSpec.describe API::Projects, :aggregate_failures, feature_category: :projects do include ProjectForksHelper include WorkhorseHelpers include StubRequests @@ -55,16 +55,14 @@ RSpec.describe API::Projects, feature_category: :projects do let_it_be(:user2) { create(:user) } let_it_be(:user3) { create(:user) } let_it_be(:admin) { create(:admin) } - let_it_be(:project, reload: true) { create(:project, :repository, create_branch: 'something_else', namespace: user.namespace) } - let_it_be(:project2, reload: true) { create(:project, namespace: user.namespace) } + let_it_be(:project, reload: true) { create(:project, :repository, create_branch: 'something_else', namespace: user.namespace, updated_at: 5.days.ago) } + let_it_be(:project2, reload: true) { create(:project, namespace: user.namespace, updated_at: 4.days.ago) } let_it_be(:project_member) { create(:project_member, :developer, user: user3, project: project) } let_it_be(:user4) { create(:user, username: 'user.withdot') } let_it_be(:project3, reload: true) do create(:project, :private, :repository, - name: 'second_project', - path: 'second_project', creator_id: user.id, namespace: user.namespace, merge_requests_enabled: false, @@ -82,8 +80,6 @@ RSpec.describe API::Projects, feature_category: :projects do let_it_be(:project4, reload: true) do create(:project, - name: 'third_project', - path: 'third_project', creator_id: user4.id, namespace: user4.namespace) end @@ -149,9 +145,15 @@ RSpec.describe API::Projects, feature_category: :projects do end describe 'GET /projects' do + let(:path) { '/projects' } + + let_it_be(:public_project) { create(:project, :public, name: 'public_project') } + shared_examples_for 'projects response' do + let_it_be(:admin_mode) { false } + it 'returns an array of projects' do - get api('/projects', current_user), params: filter + get api(path, current_user, admin_mode: admin_mode), params: filter expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -160,7 +162,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'returns the proper security headers' do - get api('/projects', current_user), params: filter + get api(path, current_user, admin_mode: admin_mode), params: filter expect(response).to include_security_headers end @@ -171,22 +173,20 @@ RSpec.describe API::Projects, feature_category: :projects do it 'avoids N + 1 queries', :use_sql_query_cache do control = ActiveRecord::QueryRecorder.new(skip_cached: false) do - get api('/projects', current_user) + get api(path, current_user) end additional_project expect do - get api('/projects', current_user) + get api(path, current_user) end.not_to exceed_all_query_limit(control).with_threshold(threshold) end end - let_it_be(:public_project) { create(:project, :public, name: 'public_project') } - context 'when unauthenticated' do it_behaves_like 'projects response' do - let(:filter) { { search: project.name } } + let(:filter) { { search: project.path } } let(:current_user) { user } let(:projects) { [project] } end @@ -208,10 +208,10 @@ RSpec.describe API::Projects, feature_category: :projects do end shared_examples 'includes container_registry_access_level' do - it do + specify do project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED) - get api('/projects', user) + get api(path, user) project_response = json_response.find { |p| p['id'] == project.id } expect(response).to have_gitlab_http_status(:ok) @@ -231,8 +231,8 @@ RSpec.describe API::Projects, feature_category: :projects do include_examples 'includes container_registry_access_level' end - it 'includes various project feature fields', :aggregate_failures do - get api('/projects', user) + it 'includes various project feature fields' do + get api(path, user) project_response = json_response.find { |p| p['id'] == project.id } expect(response).to have_gitlab_http_status(:ok) @@ -254,10 +254,10 @@ RSpec.describe API::Projects, feature_category: :projects do end end - it 'includes correct value of container_registry_enabled', :aggregate_failures do + it 'includes correct value of container_registry_enabled' do project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED) - get api('/projects', user) + get api(path, user) project_response = json_response.find { |p| p['id'] == project.id } expect(response).to have_gitlab_http_status(:ok) @@ -266,7 +266,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'includes project topics' do - get api('/projects', user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -276,7 +276,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'includes open_issues_count' do - get api('/projects', user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -287,7 +287,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'does not include projects marked for deletion' do project.update!(pending_delete: true) - get api('/projects', user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array @@ -297,7 +297,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'does not include open_issues_count if issues are disabled' do project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) - get api('/projects', user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -311,7 +311,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'returns no projects' do - get api('/projects', user), params: { topic: 'foo' } + get api(path, user), params: { topic: 'foo' } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -319,7 +319,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'returns matching project for a single topic' do - get api('/projects', user), params: { topic: 'ruby' } + get api(path, user), params: { topic: 'ruby' } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -327,7 +327,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'returns matching project for multiple topics' do - get api('/projects', user), params: { topic: 'ruby, javascript' } + get api(path, user), params: { topic: 'ruby, javascript' } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -335,7 +335,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'returns no projects if project match only some topic' do - get api('/projects', user), params: { topic: 'ruby, foo' } + get api(path, user), params: { topic: 'ruby, foo' } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -343,7 +343,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'ignores topic if it is empty' do - get api('/projects', user), params: { topic: '' } + get api(path, user), params: { topic: '' } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -404,7 +404,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it "does not include statistics by default" do - get api('/projects', user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -413,7 +413,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it "includes statistics if requested" do - get api('/projects', user), params: { statistics: true } + get api(path, user), params: { statistics: true } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -425,7 +425,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it "does not include license by default" do - get api('/projects', user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -434,7 +434,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it "does not include license if requested" do - get api('/projects', user), params: { license: true } + get api(path, user), params: { license: true } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -446,7 +446,7 @@ RSpec.describe API::Projects, feature_category: :projects do let!(:jira_integration) { create(:jira_integration, project: project) } it 'includes open_issues_count' do - get api('/projects', user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -458,7 +458,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'does not include open_issues_count if issues are disabled' do project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) - get api('/projects', user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -501,7 +501,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'returns every project' do - get api('/projects', user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -510,9 +510,38 @@ RSpec.describe API::Projects, feature_category: :projects do end end + context 'filter by updated_at' do + let(:filter) { { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago, order_by: :updated_at } } + + it_behaves_like 'projects response' do + let(:current_user) { user } + let(:projects) { [project2, project] } + end + + it 'returns projects sorted by updated_at' do + get api(path, user), params: filter + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.map { |p| p['id'] }).to match([project2, project].map(&:id)) + end + + context 'when filtering by updated_at and sorting by a different column' do + let(:filter) { { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago, order_by: 'id' } } + + it 'returns an error' do + get api(path, user), params: filter + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq( + '400 Bad request - `updated_at` filter and `updated_at` sorting must be paired' + ) + end + end + end + context 'and using search' do it_behaves_like 'projects response' do - let(:filter) { { search: project.name } } + let(:filter) { { search: project.path } } let(:current_user) { user } let(:projects) { [project] } end @@ -583,7 +612,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'and using the visibility filter' do it 'filters based on private visibility param' do - get api('/projects', user), params: { visibility: 'private' } + get api(path, user), params: { visibility: 'private' } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -594,7 +623,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'filters based on internal visibility param' do project2.update_attribute(:visibility_level, Gitlab::VisibilityLevel::INTERNAL) - get api('/projects', user), params: { visibility: 'internal' } + get api(path, user), params: { visibility: 'internal' } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -603,7 +632,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'filters based on public visibility param' do - get api('/projects', user), params: { visibility: 'public' } + get api(path, user), params: { visibility: 'public' } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -616,7 +645,7 @@ RSpec.describe API::Projects, feature_category: :projects do include_context 'with language detection' it 'filters case-insensitively by programming language' do - get api('/projects', user), params: { with_programming_language: 'javascript' } + get api(path, user), params: { with_programming_language: 'javascript' } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -627,7 +656,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'and using sorting' do it 'returns the correct order when sorted by id' do - get api('/projects', user), params: { order_by: 'id', sort: 'desc' } + get api(path, user), params: { order_by: 'id', sort: 'desc' } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -638,7 +667,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'and with owned=true' do it 'returns an array of projects the user owns' do - get api('/projects', user4), params: { owned: true } + get api(path, user4), params: { owned: true } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -659,7 +688,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'does not list as owned project for admin' do - get api('/projects', admin), params: { owned: true } + get api(path, admin, admin_mode: true), params: { owned: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_empty @@ -675,7 +704,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'returns the starred projects viewable by the user' do - get api('/projects', user3), params: { starred: true } + get api(path, user3), params: { starred: true } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -697,7 +726,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'including owned filter' do it 'returns only projects that satisfy all query parameters' do - get api('/projects', user), params: { visibility: 'public', owned: true, starred: true, search: 'gitlab' } + get api(path, user), params: { visibility: 'public', owned: true, starred: true, search: 'gitlab' } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -716,7 +745,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'returns only projects that satisfy all query parameters' do - get api('/projects', user), params: { visibility: 'public', membership: true, starred: true, search: 'gitlab' } + get api(path, user), params: { visibility: 'public', membership: true, starred: true, search: 'gitlab' } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -735,7 +764,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'returns an array of projects the user has at least developer access' do - get api('/projects', user2), params: { min_access_level: 30 } + get api(path, user2), params: { min_access_level: 30 } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -797,6 +826,7 @@ RSpec.describe API::Projects, feature_category: :projects do it_behaves_like 'projects response' do let(:filter) { {} } let(:current_user) { admin } + let(:admin_mode) { true } let(:projects) { Project.all } end end @@ -810,7 +840,7 @@ RSpec.describe API::Projects, feature_category: :projects do let(:current_user) { user } let(:params) { {} } - subject { get api('/projects', current_user), params: params } + subject(:request) { get api(path, current_user), params: params } before do group_with_projects.add_owner(current_user) if current_user @@ -818,7 +848,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'orders by id desc instead' do projects_ordered_by_id_desc = /SELECT "projects".+ORDER BY "projects"."id" DESC/i - expect { subject }.to make_queries_matching projects_ordered_by_id_desc + expect { request }.to make_queries_matching projects_ordered_by_id_desc expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -842,7 +872,7 @@ RSpec.describe API::Projects, feature_category: :projects do context "when sorting by #{order_by} ascendingly" do it 'returns a properly sorted list of projects' do - get api('/projects', current_user), params: { order_by: order_by, sort: :asc } + get api(path, current_user, admin_mode: true), params: { order_by: order_by, sort: :asc } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -853,7 +883,7 @@ RSpec.describe API::Projects, feature_category: :projects do context "when sorting by #{order_by} descendingly" do it 'returns a properly sorted list of projects' do - get api('/projects', current_user), params: { order_by: order_by, sort: :desc } + get api(path, current_user, admin_mode: true), params: { order_by: order_by, sort: :desc } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -867,7 +897,7 @@ RSpec.describe API::Projects, feature_category: :projects do let(:current_user) { user } it 'returns projects ordered normally' do - get api('/projects', current_user), params: { order_by: order_by } + get api(path, current_user), params: { order_by: order_by } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -879,7 +909,7 @@ RSpec.describe API::Projects, feature_category: :projects do end end - context 'by similarity', :aggregate_failures do + context 'by similarity' do let_it_be(:group_with_projects) { create(:group) } let_it_be(:project_1) { create(:project, name: 'Project', path: 'project', group: group_with_projects) } let_it_be(:project_2) { create(:project, name: 'Test Project', path: 'test-project', group: group_with_projects) } @@ -889,14 +919,14 @@ RSpec.describe API::Projects, feature_category: :projects do let(:current_user) { user } let(:params) { { order_by: 'similarity', search: 'test' } } - subject { get api('/projects', current_user), params: params } + subject(:request) { get api(path, current_user), params: params } before do group_with_projects.add_owner(current_user) if current_user end it 'returns non-public items based ordered by similarity' do - subject + request expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -910,14 +940,14 @@ RSpec.describe API::Projects, feature_category: :projects do let(:params) { { order_by: 'similarity' } } it 'returns items ordered by created_at descending' do - subject + request expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response.length).to eq(8) project_names = json_response.map { |proj| proj['name'] } - expect(project_names).to contain_exactly(project.name, project2.name, 'second_project', 'public_project', 'Project', 'Test Project', 'Test Public Project', 'Test') + expect(project_names).to match_array([project, project2, project3, public_project, project_1, project_2, project_4, project_3].map(&:name)) end end @@ -925,14 +955,14 @@ RSpec.describe API::Projects, feature_category: :projects do let(:current_user) { nil } it 'returns items ordered by created_at descending' do - subject + request expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response.length).to eq(1) project_names = json_response.map { |proj| proj['name'] } - expect(project_names).to contain_exactly('Test Public Project') + expect(project_names).to contain_exactly(project_4.name) end end end @@ -952,6 +982,7 @@ RSpec.describe API::Projects, feature_category: :projects do it_behaves_like 'projects response' do let(:filter) { { repository_storage: 'nfs-11' } } let(:current_user) { admin } + let(:admin_mode) { true } let(:projects) { [project, project3] } end end @@ -974,7 +1005,7 @@ RSpec.describe API::Projects, feature_category: :projects do let(:params) { { pagination: 'keyset', order_by: :id, sort: :asc, per_page: 1 } } it 'includes a pagination header with link to the next page' do - get api('/projects', current_user), params: params + get api(path, current_user), params: params expect(response.header).to include('Link') expect(response.header['Link']).to include('pagination=keyset') @@ -982,7 +1013,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'contains only the first project with per_page = 1' do - get api('/projects', current_user), params: params + get api(path, current_user), params: params expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array @@ -990,7 +1021,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'still includes a link if the end has reached and there is no more data after this page' do - get api('/projects', current_user), params: params.merge(id_after: project2.id) + get api(path, current_user), params: params.merge(id_after: project2.id) expect(response.header).to include('Link') expect(response.header['Link']).to include('pagination=keyset') @@ -998,20 +1029,20 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'does not include a next link when the page does not have any records' do - get api('/projects', current_user), params: params.merge(id_after: Project.maximum(:id)) + get api(path, current_user), params: params.merge(id_after: Project.maximum(:id)) expect(response.header).not_to include('Link') end it 'returns an empty array when the page does not have any records' do - get api('/projects', current_user), params: params.merge(id_after: Project.maximum(:id)) + get api(path, current_user), params: params.merge(id_after: Project.maximum(:id)) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to eq([]) end it 'responds with 501 if order_by is different from id' do - get api('/projects', current_user), params: params.merge(order_by: :created_at) + get api(path, current_user), params: params.merge(order_by: :created_at) expect(response).to have_gitlab_http_status(:method_not_allowed) end @@ -1021,7 +1052,7 @@ RSpec.describe API::Projects, feature_category: :projects do let(:params) { { pagination: 'keyset', order_by: :id, sort: :desc, per_page: 1 } } it 'includes a pagination header with link to the next page' do - get api('/projects', current_user), params: params + get api(path, current_user), params: params expect(response.header).to include('Link') expect(response.header['Link']).to include('pagination=keyset') @@ -1029,7 +1060,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'contains only the last project with per_page = 1' do - get api('/projects', current_user), params: params + get api(path, current_user), params: params expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array @@ -1041,7 +1072,7 @@ RSpec.describe API::Projects, feature_category: :projects do let(:params) { { pagination: 'keyset', order_by: :id, sort: :desc, per_page: 2 } } it 'returns all projects' do - url = '/projects' + url = path requests = 0 ids = [] @@ -1067,8 +1098,11 @@ RSpec.describe API::Projects, feature_category: :projects do let_it_be(:admin) { create(:admin) } + subject(:request) { get api(path, admin) } + it 'avoids N+1 queries', :use_sql_query_cache do - get api('/projects', admin) + request + expect(response).to have_gitlab_http_status(:ok) base_project = create(:project, :public, namespace: admin.namespace) @@ -1076,53 +1110,94 @@ RSpec.describe API::Projects, feature_category: :projects do fork_project2 = fork_project(fork_project1, admin, namespace: create(:user).namespace) control = ActiveRecord::QueryRecorder.new(skip_cached: false) do - get api('/projects', admin) + request end fork_project(fork_project2, admin, namespace: create(:user).namespace) expect do - get api('/projects', admin) - end.not_to exceed_query_limit(control.count) + request + end.not_to exceed_all_query_limit(control.count) end end context 'when service desk is enabled', :use_clean_rails_memory_store_caching do let_it_be(:admin) { create(:admin) } + subject(:request) { get api(path, admin) } + it 'avoids N+1 queries' do - allow(Gitlab::ServiceDeskEmail).to receive(:enabled?).and_return(true) - allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(true) + allow(Gitlab::Email::ServiceDeskEmail).to receive(:enabled?).and_return(true) + allow(Gitlab::Email::IncomingEmail).to receive(:enabled?).and_return(true) - get api('/projects', admin) + request + expect(response).to have_gitlab_http_status(:ok) create(:project, :public, :service_desk_enabled, namespace: admin.namespace) control = ActiveRecord::QueryRecorder.new do - get api('/projects', admin) + request end create_list(:project, 2, :public, :service_desk_enabled, namespace: admin.namespace) expect do - get api('/projects', admin) - end.not_to exceed_query_limit(control) + request + end.not_to exceed_all_query_limit(control) + end + end + + context 'rate limiting' do + let_it_be(:current_user) { create(:user) } + + shared_examples_for 'does not log request and does not block the request' do + specify do + request + request + + expect(response).not_to have_gitlab_http_status(:too_many_requests) + expect(Gitlab::AuthLogger).not_to receive(:error) + end + end + + before do + stub_application_setting(projects_api_rate_limit_unauthenticated: 1) + end + + context 'when the user is signed in' do + it_behaves_like 'does not log request and does not block the request' do + def request + get api(path, current_user) + end + end + end + + context 'when the user is not signed in' do + let_it_be(:current_user) { nil } + + it_behaves_like 'rate limited endpoint', rate_limit_key: :projects_api_rate_limit_unauthenticated do + def request + get api(path, current_user) + end + end end end end describe 'POST /projects' do + let(:path) { '/projects' } + context 'maximum number of projects reached' do it 'does not create new project and respond with 403' do allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0) - expect { post api('/projects', user2), params: { name: 'foo' } } + expect { post api(path, user2), params: { name: 'foo' } } .to change { Project.count }.by(0) expect(response).to have_gitlab_http_status(:forbidden) end end it 'creates new project without path but with name and returns 201' do - expect { post api('/projects', user), params: { name: 'Foo Project' } } + expect { post api(path, user), params: { name: 'Foo Project' } } .to change { Project.count }.by(1) expect(response).to have_gitlab_http_status(:created) @@ -1133,7 +1208,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'creates new project without name but with path and returns 201' do - expect { post api('/projects', user), params: { path: 'foo_project' } } + expect { post api(path, user), params: { path: 'foo_project' } } .to change { Project.count }.by(1) expect(response).to have_gitlab_http_status(:created) @@ -1144,7 +1219,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'creates new project with name and path and returns 201' do - expect { post api('/projects', user), params: { path: 'path-project-Foo', name: 'Foo Project' } } + expect { post api(path, user), params: { path: 'path-project-Foo', name: 'Foo Project' } } .to change { Project.count }.by(1) expect(response).to have_gitlab_http_status(:created) @@ -1155,21 +1230,21 @@ RSpec.describe API::Projects, feature_category: :projects do end it_behaves_like 'create project with default branch parameter' do - let(:request) { post api('/projects', user), params: params } + subject(:request) { post api(path, user), params: params } end it 'creates last project before reaching project limit' do allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1) - post api('/projects', user2), params: { name: 'foo' } + post api(path, user2), params: { name: 'foo' } expect(response).to have_gitlab_http_status(:created) end it 'does not create new project without name or path and returns 400' do - expect { post api('/projects', user) }.not_to change { Project.count } + expect { post api(path, user) }.not_to change { Project.count } expect(response).to have_gitlab_http_status(:bad_request) end - it 'assigns attributes to project', :aggregate_failures do + it 'assigns attributes to project' do project = attributes_for(:project, { path: 'camelCasePath', issues_enabled: false, @@ -1189,7 +1264,6 @@ RSpec.describe API::Projects, feature_category: :projects do merge_method: 'ff', squash_option: 'always' }).tap do |attrs| - attrs[:operations_access_level] = 'disabled' attrs[:analytics_access_level] = 'disabled' attrs[:container_registry_access_level] = 'private' attrs[:security_and_compliance_access_level] = 'private' @@ -1205,7 +1279,7 @@ RSpec.describe API::Projects, feature_category: :projects do attrs[:issues_access_level] = 'disabled' end - post api('/projects', user), params: project + post api(path, user), params: project expect(response).to have_gitlab_http_status(:created) @@ -1224,7 +1298,6 @@ RSpec.describe API::Projects, feature_category: :projects do expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED) expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED) expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED) - expect(project.operations_access_level).to eq(ProjectFeature::DISABLED) expect(project.project_feature.analytics_access_level).to eq(ProjectFeature::DISABLED) expect(project.project_feature.container_registry_access_level).to eq(ProjectFeature::PRIVATE) expect(project.project_feature.security_and_compliance_access_level).to eq(ProjectFeature::PRIVATE) @@ -1240,10 +1313,10 @@ RSpec.describe API::Projects, feature_category: :projects do expect(project.project_feature.snippets_access_level).to eq(ProjectFeature::DISABLED) end - it 'assigns container_registry_enabled to project', :aggregate_failures do + it 'assigns container_registry_enabled to project' do project = attributes_for(:project, { container_registry_enabled: true }) - post api('/projects', user), params: project + post api(path, user), params: project expect(response).to have_gitlab_http_status(:created) expect(json_response['container_registry_enabled']).to eq(true) @@ -1254,7 +1327,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'assigns container_registry_enabled to project' do project = attributes_for(:project, { container_registry_enabled: true }) - post api('/projects', user), params: project + post api(path, user), params: project expect(response).to have_gitlab_http_status(:created) expect(json_response['container_registry_enabled']).to eq(true) @@ -1262,7 +1335,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'creates a project using a template' do - expect { post api('/projects', user), params: { template_name: 'rails', name: 'rails-test' } } + expect { post api(path, user), params: { template_name: 'rails', name: 'rails-test' } } .to change { Project.count }.by(1) expect(response).to have_gitlab_http_status(:created) @@ -1273,7 +1346,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'returns 400 for an invalid template' do - expect { post api('/projects', user), params: { template_name: 'unknown', name: 'rails-test' } } + expect { post api(path, user), params: { template_name: 'unknown', name: 'rails-test' } } .not_to change { Project.count } expect(response).to have_gitlab_http_status(:bad_request) @@ -1282,7 +1355,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'disallows creating a project with an import_url and template' do project_params = { import_url: 'http://example.com', template_name: 'rails', name: 'rails-test' } - expect { post api('/projects', user), params: project_params } + expect { post api(path, user), params: project_params } .not_to change { Project.count } expect(response).to have_gitlab_http_status(:bad_request) @@ -1299,34 +1372,34 @@ RSpec.describe API::Projects, feature_category: :projects do headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } }) project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' } - expect { post api('/projects', user), params: project_params } + expect { post api(path, user), params: project_params } .not_to change { Project.count } expect(response).to have_gitlab_http_status(:forbidden) end - it 'allows creating a project without an import_url when git import source is disabled', :aggregate_failures do + it 'allows creating a project without an import_url when git import source is disabled' do stub_application_setting(import_sources: nil) project_params = { path: 'path-project-Foo' } - expect { post api('/projects', user), params: project_params }.to change { Project.count }.by(1) + expect { post api(path, user), params: project_params }.to change { Project.count }.by(1) expect(response).to have_gitlab_http_status(:created) end - it 'disallows creating a project with an import_url that is not reachable', :aggregate_failures do + it 'disallows creating a project with an import_url that is not reachable' do url = 'http://example.com' endpoint_url = "#{url}/info/refs?service=git-upload-pack" stub_full_request(endpoint_url, method: :get).to_return({ status: 301, body: '', headers: nil }) project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' } - expect { post api('/projects', user), params: project_params }.not_to change { Project.count } + expect { post api(path, user), params: project_params }.not_to change { Project.count } expect(response).to have_gitlab_http_status(:unprocessable_entity) expect(json_response['message']).to eq("#{url} is not a valid HTTP Git repository") end - it 'creates a project with an import_url that is valid', :aggregate_failures do + it 'creates a project with an import_url that is valid' do url = 'http://example.com' endpoint_url = "#{url}/info/refs?service=git-upload-pack" git_response = { @@ -1334,10 +1407,11 @@ RSpec.describe API::Projects, feature_category: :projects do body: '001e# service=git-upload-pack', headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } } + stub_application_setting(import_sources: ['git']) stub_full_request(endpoint_url, method: :get).to_return(git_response) project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' } - expect { post api('/projects', user), params: project_params }.to change { Project.count }.by(1) + expect { post api(path, user), params: project_params }.to change { Project.count }.by(1) expect(response).to have_gitlab_http_status(:created) end @@ -1345,7 +1419,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as public' do project = attributes_for(:project, visibility: 'public') - post api('/projects', user), params: project + post api(path, user), params: project expect(json_response['visibility']).to eq('public') end @@ -1353,7 +1427,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as internal' do project = attributes_for(:project, visibility: 'internal') - post api('/projects', user), params: project + post api(path, user), params: project expect(json_response['visibility']).to eq('internal') end @@ -1361,23 +1435,23 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as private' do project = attributes_for(:project, visibility: 'private') - post api('/projects', user), params: project + post api(path, user), params: project expect(json_response['visibility']).to eq('private') end it 'creates a new project initialized with a README.md' do - project = attributes_for(:project, initialize_with_readme: 1, name: 'somewhere') + project = attributes_for(:project, initialize_with_readme: 1) - post api('/projects', user), params: project + post api(path, user), params: project - expect(json_response['readme_url']).to eql("#{Gitlab.config.gitlab.url}/#{json_response['namespace']['full_path']}/somewhere/-/blob/master/README.md") + expect(json_response['readme_url']).to eql("#{Gitlab.config.gitlab.url}/#{json_response['namespace']['full_path']}/#{json_response['path']}/-/blob/master/README.md") end it 'sets tag list to a project (deprecated)' do project = attributes_for(:project, tag_list: %w[tagFirst tagSecond]) - post api('/projects', user), params: project + post api(path, user), params: project expect(json_response['topics']).to eq(%w[tagFirst tagSecond]) end @@ -1385,7 +1459,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets topics to a project' do project = attributes_for(:project, topics: %w[topic1 topics2]) - post api('/projects', user), params: project + post api(path, user), params: project expect(json_response['topics']).to eq(%w[topic1 topics2]) end @@ -1394,7 +1468,7 @@ RSpec.describe API::Projects, feature_category: :projects do project = attributes_for(:project, avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif')) workhorse_form_with_file( - api('/projects', user), + api(path, user), method: :post, file_key: :avatar, params: project @@ -1407,7 +1481,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as not allowing outdated diff discussions to automatically resolve' do project = attributes_for(:project, resolve_outdated_diff_discussions: false) - post api('/projects', user), params: project + post api(path, user), params: project expect(json_response['resolve_outdated_diff_discussions']).to be_falsey end @@ -1415,7 +1489,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as allowing outdated diff discussions to automatically resolve' do project = attributes_for(:project, resolve_outdated_diff_discussions: true) - post api('/projects', user), params: project + post api(path, user), params: project expect(json_response['resolve_outdated_diff_discussions']).to be_truthy end @@ -1423,7 +1497,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as not removing source branches' do project = attributes_for(:project, remove_source_branch_after_merge: false) - post api('/projects', user), params: project + post api(path, user), params: project expect(json_response['remove_source_branch_after_merge']).to be_falsey end @@ -1431,7 +1505,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as removing source branches' do project = attributes_for(:project, remove_source_branch_after_merge: true) - post api('/projects', user), params: project + post api(path, user), params: project expect(json_response['remove_source_branch_after_merge']).to be_truthy end @@ -1439,7 +1513,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as allowing merge even if build fails' do project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: false) - post api('/projects', user), params: project + post api(path, user), params: project expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey end @@ -1447,7 +1521,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: true) - post api('/projects', user), params: project + post api(path, user), params: project expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy end @@ -1455,7 +1529,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as not allowing merge when pipeline is skipped' do project_params = attributes_for(:project, allow_merge_on_skipped_pipeline: false) - post api('/projects', user), params: project_params + post api(path, user), params: project_params expect(json_response['allow_merge_on_skipped_pipeline']).to be_falsey end @@ -1463,7 +1537,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as allowing merge when pipeline is skipped' do project_params = attributes_for(:project, allow_merge_on_skipped_pipeline: true) - post api('/projects', user), params: project_params + post api(path, user), params: project_params expect(json_response['allow_merge_on_skipped_pipeline']).to be_truthy end @@ -1471,7 +1545,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as allowing merge even if discussions are unresolved' do project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: false) - post api('/projects', user), params: project + post api(path, user), params: project expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey end @@ -1479,7 +1553,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as allowing merge if only_allow_merge_if_all_discussions_are_resolved is nil' do project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: nil) - post api('/projects', user), params: project + post api(path, user), params: project expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey end @@ -1487,7 +1561,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as allowing merge only if all discussions are resolved' do project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: true) - post api('/projects', user), params: project + post api(path, user), params: project expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy end @@ -1495,7 +1569,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as enabling auto close referenced issues' do project = attributes_for(:project, autoclose_referenced_issues: true) - post api('/projects', user), params: project + post api(path, user), params: project expect(json_response['autoclose_referenced_issues']).to be_truthy end @@ -1503,7 +1577,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as disabling auto close referenced issues' do project = attributes_for(:project, autoclose_referenced_issues: false) - post api('/projects', user), params: project + post api(path, user), params: project expect(json_response['autoclose_referenced_issues']).to be_falsey end @@ -1511,7 +1585,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets the merge method of a project to rebase merge' do project = attributes_for(:project, merge_method: 'rebase_merge') - post api('/projects', user), params: project + post api(path, user), params: project expect(json_response['merge_method']).to eq('rebase_merge') end @@ -1519,7 +1593,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'rejects invalid values for merge_method' do project = attributes_for(:project, merge_method: 'totally_not_valid_method') - post api('/projects', user), params: project + post api(path, user), params: project expect(response).to have_gitlab_http_status(:bad_request) end @@ -1527,7 +1601,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'ignores import_url when it is nil' do project = attributes_for(:project, import_url: nil) - post api('/projects', user), params: project + post api(path, user), params: project expect(response).to have_gitlab_http_status(:created) end @@ -1540,7 +1614,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'does not allow a non-admin to use a restricted visibility level' do - post api('/projects', user), params: project_param + post api(path, user), params: project_param expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']['visibility_level'].first).to( @@ -1549,7 +1623,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'allows an admin to override restricted visibility settings' do - post api('/projects', admin), params: project_param + post api(path, admin), params: project_param expect(json_response['visibility']).to eq('public') end @@ -1557,7 +1631,7 @@ RSpec.describe API::Projects, feature_category: :projects do end describe 'GET /users/:user_id/projects/' do - let!(:public_project) { create(:project, :public, name: 'public_project', creator_id: user4.id, namespace: user4.namespace) } + let_it_be(:public_project) { create(:project, :public, creator_id: user4.id, namespace: user4.namespace) } it 'returns error when user not found' do get api("/users/#{non_existing_record_id}/projects/") @@ -1575,7 +1649,7 @@ RSpec.describe API::Projects, feature_category: :projects do expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id) end - it 'includes container_registry_access_level', :aggregate_failures do + it 'includes container_registry_access_level' do get api("/users/#{user4.id}/projects/", user) expect(response).to have_gitlab_http_status(:ok) @@ -1583,8 +1657,18 @@ RSpec.describe API::Projects, feature_category: :projects do expect(json_response.first.keys).to include('container_registry_access_level') end + context 'filter by updated_at' do + it 'returns only projects updated on the given timeframe' do + get api("/users/#{user.id}/projects", user), + params: { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.map { |project| project['id'] }).to contain_exactly(project2.id, project.id) + end + end + context 'and using id_after' do - let!(:another_public_project) { create(:project, :public, name: 'another_public_project', creator_id: user4.id, namespace: user4.namespace) } + let_it_be(:another_public_project) { create(:project, :public, creator_id: user4.id, namespace: user4.namespace) } it 'only returns projects with id_after filter given' do get api("/users/#{user4.id}/projects?id_after=#{public_project.id}", user) @@ -1606,7 +1690,7 @@ RSpec.describe API::Projects, feature_category: :projects do end context 'and using id_before' do - let!(:another_public_project) { create(:project, :public, name: 'another_public_project', creator_id: user4.id, namespace: user4.namespace) } + let_it_be(:another_public_project) { create(:project, :public, creator_id: user4.id, namespace: user4.namespace) } it 'only returns projects with id_before filter given' do get api("/users/#{user4.id}/projects?id_before=#{another_public_project.id}", user) @@ -1628,7 +1712,7 @@ RSpec.describe API::Projects, feature_category: :projects do end context 'and using both id_before and id_after' do - let!(:more_projects) { create_list(:project, 5, :public, creator_id: user4.id, namespace: user4.namespace) } + let_it_be(:more_projects) { create_list(:project, 5, :public, creator_id: user4.id, namespace: user4.namespace) } it 'only returns projects with id matching the range' do get api("/users/#{user4.id}/projects?id_after=#{more_projects.first.id}&id_before=#{more_projects.last.id}", user) @@ -1663,7 +1747,7 @@ RSpec.describe API::Projects, feature_category: :projects do expect(json_response.map { |project| project['id'] }).to contain_exactly(private_project1.id) end - context 'and using an admin to search', :enable_admin_mode, :aggregate_errors do + context 'and using an admin to search', :enable_admin_mode do it 'returns users projects when authenticated as admin' do private_project1 = create(:project, :private, name: 'private_project1', creator_id: user4.id, namespace: user4.namespace) @@ -1697,6 +1781,8 @@ RSpec.describe API::Projects, feature_category: :projects do user3.reload end + let(:path) { "/users/#{user3.id}/starred_projects/" } + it 'returns error when user not found' do get api("/users/#{non_existing_record_id}/starred_projects/") @@ -1706,7 +1792,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'with a public profile' do it 'returns projects filtered by user' do - get api("/users/#{user3.id}/starred_projects/", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -1714,6 +1800,16 @@ RSpec.describe API::Projects, feature_category: :projects do expect(json_response.map { |project| project['id'] }) .to contain_exactly(project.id, project2.id, project3.id) end + + context 'filter by updated_at' do + it 'returns only projects updated on the given timeframe' do + get api(path, user), + params: { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.map { |project| project['id'] }).to contain_exactly(project2.id, project.id) + end + end end context 'with a private profile' do @@ -1724,7 +1820,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'user does not have access to view the private profile' do it 'returns no projects' do - get api("/users/#{user3.id}/starred_projects/", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -1735,7 +1831,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'user has access to view the private profile' do it 'returns projects filtered by user' do - get api("/users/#{user3.id}/starred_projects/", admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -1748,8 +1844,14 @@ RSpec.describe API::Projects, feature_category: :projects do end describe 'POST /projects/user/:id' do + let(:path) { "/projects/user/#{user.id}" } + + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { { name: 'Foo Project' } } + end + it 'creates new project without path but with name and return 201' do - expect { post api("/projects/user/#{user.id}", admin), params: { name: 'Foo Project' } }.to change { Project.count }.by(1) + expect { post api(path, admin, admin_mode: true), params: { name: 'Foo Project' } }.to change { Project.count }.by(1) expect(response).to have_gitlab_http_status(:created) project = Project.find(json_response['id']) @@ -1759,7 +1861,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'creates new project with name and path and returns 201' do - expect { post api("/projects/user/#{user.id}", admin), params: { path: 'path-project-Foo', name: 'Foo Project' } } + expect { post api(path, admin, admin_mode: true), params: { path: 'path-project-Foo', name: 'Foo Project' } } .to change { Project.count }.by(1) expect(response).to have_gitlab_http_status(:created) @@ -1770,11 +1872,11 @@ RSpec.describe API::Projects, feature_category: :projects do end it_behaves_like 'create project with default branch parameter' do - let(:request) { post api("/projects/user/#{user.id}", admin), params: params } + subject(:request) { post api(path, admin, admin_mode: true), params: params } end it 'responds with 400 on failure and not project' do - expect { post api("/projects/user/#{user.id}", admin) } + expect { post api(path, admin, admin_mode: true) } .not_to change { Project.count } expect(response).to have_gitlab_http_status(:bad_request) @@ -1786,7 +1888,7 @@ RSpec.describe API::Projects, feature_category: :projects do attrs[:container_registry_enabled] = true end - post api("/projects/user/#{user.id}", admin), params: project + post api(path, admin, admin_mode: true), params: project expect(response).to have_gitlab_http_status(:created) expect(json_response['container_registry_enabled']).to eq(true) @@ -1802,7 +1904,7 @@ RSpec.describe API::Projects, feature_category: :projects do jobs_enabled: true }) - post api("/projects/user/#{user.id}", admin), params: project + post api(path, admin, admin_mode: true), params: project expect(response).to have_gitlab_http_status(:created) @@ -1816,7 +1918,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as public' do project = attributes_for(:project, visibility: 'public') - post api("/projects/user/#{user.id}", admin), params: project + post api(path, admin, admin_mode: true), params: project expect(response).to have_gitlab_http_status(:created) expect(json_response['visibility']).to eq('public') @@ -1825,7 +1927,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as internal' do project = attributes_for(:project, visibility: 'internal') - post api("/projects/user/#{user.id}", admin), params: project + post api(path, admin, admin_mode: true), params: project expect(response).to have_gitlab_http_status(:created) expect(json_response['visibility']).to eq('internal') @@ -1834,7 +1936,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as private' do project = attributes_for(:project, visibility: 'private') - post api("/projects/user/#{user.id}", admin), params: project + post api(path, admin, admin_mode: true), params: project expect(json_response['visibility']).to eq('private') end @@ -1842,7 +1944,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as not allowing outdated diff discussions to automatically resolve' do project = attributes_for(:project, resolve_outdated_diff_discussions: false) - post api("/projects/user/#{user.id}", admin), params: project + post api(path, admin, admin_mode: true), params: project expect(json_response['resolve_outdated_diff_discussions']).to be_falsey end @@ -1850,7 +1952,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as allowing outdated diff discussions to automatically resolve' do project = attributes_for(:project, resolve_outdated_diff_discussions: true) - post api("/projects/user/#{user.id}", admin), params: project + post api(path, admin, admin_mode: true), params: project expect(json_response['resolve_outdated_diff_discussions']).to be_truthy end @@ -1858,7 +1960,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as not removing source branches' do project = attributes_for(:project, remove_source_branch_after_merge: false) - post api("/projects/user/#{user.id}", admin), params: project + post api(path, admin, admin_mode: true), params: project expect(json_response['remove_source_branch_after_merge']).to be_falsey end @@ -1866,7 +1968,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as removing source branches' do project = attributes_for(:project, remove_source_branch_after_merge: true) - post api("/projects/user/#{user.id}", admin), params: project + post api(path, admin, admin_mode: true), params: project expect(json_response['remove_source_branch_after_merge']).to be_truthy end @@ -1874,7 +1976,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as allowing merge even if build fails' do project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: false) - post api("/projects/user/#{user.id}", admin), params: project + post api(path, admin, admin_mode: true), params: project expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey end @@ -1882,7 +1984,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as allowing merge only if pipeline succeeds' do project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: true) - post api("/projects/user/#{user.id}", admin), params: project + post api(path, admin, admin_mode: true), params: project expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy end @@ -1890,7 +1992,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as not allowing merge when pipeline is skipped' do project = attributes_for(:project, allow_merge_on_skipped_pipeline: false) - post api("/projects/user/#{user.id}", admin), params: project + post api(path, admin, admin_mode: true), params: project expect(json_response['allow_merge_on_skipped_pipeline']).to be_falsey end @@ -1898,7 +2000,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as allowing merge when pipeline is skipped' do project = attributes_for(:project, allow_merge_on_skipped_pipeline: true) - post api("/projects/user/#{user.id}", admin), params: project + post api(path, admin, admin_mode: true), params: project expect(json_response['allow_merge_on_skipped_pipeline']).to be_truthy end @@ -1906,7 +2008,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as allowing merge even if discussions are unresolved' do project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: false) - post api("/projects/user/#{user.id}", admin), params: project + post api(path, admin, admin_mode: true), params: project expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey end @@ -1914,7 +2016,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets a project as allowing merge only if all discussions are resolved' do project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: true) - post api("/projects/user/#{user.id}", admin), params: project + post api(path, admin, admin_mode: true), params: project expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy end @@ -1928,12 +2030,12 @@ RSpec.describe API::Projects, feature_category: :projects do end with_them do - it 'setting container_registry_enabled also sets container_registry_access_level', :aggregate_failures do + it 'setting container_registry_enabled also sets container_registry_access_level' do project_attributes = attributes_for(:project).tap do |attrs| attrs[:container_registry_enabled] = container_registry_enabled end - post api("/projects/user/#{user.id}", admin), params: project_attributes + post api(path, admin, admin_mode: true), params: project_attributes project = Project.find_by(path: project_attributes[:path]) expect(response).to have_gitlab_http_status(:created) @@ -1955,12 +2057,12 @@ RSpec.describe API::Projects, feature_category: :projects do end with_them do - it 'setting container_registry_access_level also sets container_registry_enabled', :aggregate_failures do + it 'setting container_registry_access_level also sets container_registry_enabled' do project_attributes = attributes_for(:project).tap do |attrs| attrs[:container_registry_access_level] = container_registry_access_level end - post api("/projects/user/#{user.id}", admin), params: project_attributes + post api(path, admin, admin_mode: true), params: project_attributes project = Project.find_by(path: project_attributes[:path]) expect(response).to have_gitlab_http_status(:created) @@ -1975,10 +2077,11 @@ RSpec.describe API::Projects, feature_category: :projects do describe "POST /projects/:id/uploads/authorize" do let(:headers) { workhorse_internal_api_request_header.merge({ 'HTTP_GITLAB_WORKHORSE' => 1 }) } + let(:path) { "/projects/#{project.id}/uploads/authorize" } context 'with authorized user' do it "returns 200" do - post api("/projects/#{project.id}/uploads/authorize", user), headers: headers + post api(path, user), headers: headers expect(response).to have_gitlab_http_status(:ok) expect(json_response['MaximumSize']).to eq(project.max_attachment_size) @@ -1987,7 +2090,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'with unauthorized user' do it "returns 404" do - post api("/projects/#{project.id}/uploads/authorize", user2), headers: headers + post api(path, user2), headers: headers expect(response).to have_gitlab_http_status(:not_found) end @@ -1999,20 +2102,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it "returns 200" do - post api("/projects/#{project.id}/uploads/authorize", user), headers: headers - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['MaximumSize']).to eq(1.gigabyte) - end - end - - context 'with upload size enforcement disabled' do - before do - stub_feature_flags(enforce_max_attachment_size_upload_api: false) - end - - it "returns 200" do - post api("/projects/#{project.id}/uploads/authorize", user), headers: headers + post api(path, user), headers: headers expect(response).to have_gitlab_http_status(:ok) expect(json_response['MaximumSize']).to eq(1.gigabyte) @@ -2021,7 +2111,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'with no Workhorse headers' do it "returns 403" do - post api("/projects/#{project.id}/uploads/authorize", user) + post api(path, user) expect(response).to have_gitlab_http_status(:forbidden) end @@ -2030,6 +2120,7 @@ RSpec.describe API::Projects, feature_category: :projects do describe "POST /projects/:id/uploads" do let(:file) { fixture_file_upload("spec/fixtures/dk.png", "image/png") } + let(:path) { "/projects/#{project.id}/uploads" } before do project @@ -2040,7 +2131,7 @@ RSpec.describe API::Projects, feature_category: :projects do expect(instance).to receive(:override_max_attachment_size=).with(project.max_attachment_size).and_call_original end - post api("/projects/#{project.id}/uploads", user), params: { file: file } + post api(path, user), params: { file: file } expect(response).to have_gitlab_http_status(:created) expect(json_response['alt']).to eq("dk") @@ -2060,7 +2151,7 @@ RSpec.describe API::Projects, feature_category: :projects do expect(path).not_to be(nil) expect(Rack::Multipart::Parser::TEMPFILE_FACTORY).to receive(:call).and_return(tempfile) - post api("/projects/#{project.id}/uploads", user), params: { file: fixture_file_upload("spec/fixtures/dk.png", "image/png") } + post api(path, user), params: { file: fixture_file_upload("spec/fixtures/dk.png", "image/png") } expect(tempfile.path).to be(nil) expect(File.exist?(path)).to be(false) @@ -2072,7 +2163,7 @@ RSpec.describe API::Projects, feature_category: :projects do expect(instance).to receive(:override_max_attachment_size=).with(1.gigabyte).and_call_original end - post api("/projects/#{project.id}/uploads", user), params: { file: file } + post api(path, user), params: { file: file } expect(response).to have_gitlab_http_status(:created) end @@ -2084,7 +2175,7 @@ RSpec.describe API::Projects, feature_category: :projects do hash_including(message: 'File exceeds maximum size', upload_allowed: upload_allowed)) .and_call_original - post api("/projects/#{project.id}/uploads", user), params: { file: file } + post api(path, user), params: { file: file } end end @@ -2095,14 +2186,6 @@ RSpec.describe API::Projects, feature_category: :projects do it_behaves_like 'capped upload attachments', true end - - context 'with upload size enforcement disabled' do - before do - stub_feature_flags(enforce_max_attachment_size_upload_api: false) - end - - it_behaves_like 'capped upload attachments', false - end end describe "GET /projects/:id/groups" do @@ -2113,33 +2196,37 @@ RSpec.describe API::Projects, feature_category: :projects do let_it_be(:private_project) { create(:project, :private, group: project_group) } let_it_be(:public_project) { create(:project, :public, group: project_group) } + let(:path) { "/projects/#{private_project.id}/groups" } + before_all do create(:project_group_link, :developer, group: shared_group_with_dev_access, project: private_project) create(:project_group_link, :reporter, group: shared_group_with_reporter_access, project: private_project) end + it_behaves_like 'GET request permissions for admin mode' do + let(:failed_status_code) { :not_found } + end + shared_examples_for 'successful groups response' do it 'returns an array of groups' do request - aggregate_failures do - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.map { |g| g['name'] }).to match_array(expected_groups.map(&:name)) - end + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.map { |g| g['name'] }).to match_array(expected_groups.map(&:name)) end end context 'when unauthenticated' do it 'does not return groups for private projects' do - get api("/projects/#{private_project.id}/groups") + get api(path) expect(response).to have_gitlab_http_status(:not_found) end context 'for public projects' do - let(:request) { get api("/projects/#{public_project.id}/groups") } + subject(:request) { get api("/projects/#{public_project.id}/groups") } it_behaves_like 'successful groups response' do let(:expected_groups) { [root_group, project_group] } @@ -2150,14 +2237,15 @@ RSpec.describe API::Projects, feature_category: :projects do context 'when authenticated as user' do context 'when user does not have access to the project' do it 'does not return groups' do - get api("/projects/#{private_project.id}/groups", user) + get api(path, user) expect(response).to have_gitlab_http_status(:not_found) end end context 'when user has access to the project' do - let(:request) { get api("/projects/#{private_project.id}/groups", user), params: params } + subject(:request) { get api(path, user), params: params } + let(:params) { {} } before do @@ -2219,7 +2307,7 @@ RSpec.describe API::Projects, feature_category: :projects do end context 'when authenticated as admin' do - let(:request) { get api("/projects/#{private_project.id}/groups", admin) } + subject(:request) { get api(path, admin, admin_mode: true) } it_behaves_like 'successful groups response' do let(:expected_groups) { [root_group, project_group] } @@ -2228,27 +2316,30 @@ RSpec.describe API::Projects, feature_category: :projects do end describe 'GET /project/:id/share_locations' do - let_it_be(:root_group) { create(:group, :public, name: 'root group') } - let_it_be(:project_group1) { create(:group, :public, parent: root_group, name: 'group1') } - let_it_be(:project_group2) { create(:group, :public, parent: root_group, name: 'group2') } + let_it_be(:root_group) { create(:group, :public, name: 'root group', path: 'root-group-path') } + let_it_be(:project_group1) { create(:group, :public, parent: root_group, name: 'group1', path: 'group-1-path') } + let_it_be(:project_group2) { create(:group, :public, parent: root_group, name: 'group2', path: 'group-2-path') } let_it_be(:project) { create(:project, :private, group: project_group1) } + let(:path) { "/projects/#{project.id}/share_locations" } + + it_behaves_like 'GET request permissions for admin mode' do + let(:failed_status_code) { :not_found } + end shared_examples_for 'successful groups response' do it 'returns an array of groups' do request - aggregate_failures do - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.map { |g| g['name'] }).to match_array(expected_groups.map(&:name)) - end + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.map { |g| g['name'] }).to match_array(expected_groups.map(&:name)) end end context 'when unauthenticated' do it 'does not return the groups for the given project' do - get api("/projects/#{project.id}/share_locations") + get api(path) expect(response).to have_gitlab_http_status(:not_found) end @@ -2257,14 +2348,15 @@ RSpec.describe API::Projects, feature_category: :projects do context 'when authenticated' do context 'when user is not the owner of the project' do it 'does not return the groups' do - get api("/projects/#{project.id}/share_locations", user) + get api(path, user) expect(response).to have_gitlab_http_status(:not_found) end end context 'when user is the owner of the project' do - let(:request) { get api("/projects/#{project.id}/share_locations", user), params: params } + subject(:request) { get api(path, user), params: params } + let(:params) { {} } before do @@ -2275,26 +2367,38 @@ RSpec.describe API::Projects, feature_category: :projects do context 'with default search' do it_behaves_like 'successful groups response' do - let(:expected_groups) { [project_group1, project_group2] } + let(:expected_groups) { [project_group2] } end end context 'when searching by group name' do - let(:params) { { search: 'group1' } } + context 'searching by group name' do + it_behaves_like 'successful groups response' do + let(:params) { { search: 'group2' } } + let(:expected_groups) { [project_group2] } + end + end - it_behaves_like 'successful groups response' do - let(:expected_groups) { [project_group1] } + context 'searching by full group path' do + let_it_be(:project_group2_subgroup) do + create(:group, :public, parent: project_group2, name: 'subgroup', path: 'subgroup-path') + end + + it_behaves_like 'successful groups response' do + let(:params) { { search: 'root-group-path/group-2-path/subgroup-path' } } + let(:expected_groups) { [project_group2_subgroup] } + end end end end end context 'when authenticated as admin' do - let(:request) { get api("/projects/#{project.id}/share_locations", admin), params: {} } + subject(:request) { get api(path, admin, admin_mode: true), params: {} } context 'without share_with_group_lock' do it_behaves_like 'successful groups response' do - let(:expected_groups) { [root_group, project_group1, project_group2] } + let(:expected_groups) { [project_group2] } end end @@ -2311,6 +2415,12 @@ RSpec.describe API::Projects, feature_category: :projects do end describe 'GET /projects/:id' do + let(:path) { "/projects/#{project.id}" } + + it_behaves_like 'GET request permissions for admin mode' do + let(:failed_status_code) { :not_found } + end + context 'when unauthenticated' do it 'does not return private projects' do private_project = create(:project, :private) @@ -2350,7 +2460,7 @@ RSpec.describe API::Projects, feature_category: :projects do let(:protected_attributes) { %w(default_branch ci_config_path) } it 'hides protected attributes of private repositories if user is not a member' do - get api("/projects/#{project.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) protected_attributes.each do |attribute| @@ -2361,7 +2471,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'exposes protected attributes of private repositories if user is a member' do project.add_developer(user) - get api("/projects/#{project.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) protected_attributes.each do |attribute| @@ -2408,17 +2518,18 @@ RSpec.describe API::Projects, feature_category: :projects do keys end - it 'returns a project by id', :aggregate_failures do + it 'returns a project by id' do project project_member group = create(:group) link = create(:project_group_link, project: project, group: group) - get api("/projects/#{project.id}", admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['id']).to eq(project.id) expect(json_response['description']).to eq(project.description) + expect(json_response['description_html']).to eq(project.description_html) expect(json_response['default_branch']).to eq(project.default_branch) expect(json_response['tag_list']).to be_an Array # deprecated in favor of 'topics' expect(json_response['topics']).to be_an Array @@ -2440,6 +2551,7 @@ RSpec.describe API::Projects, feature_category: :projects do expect(json_response['container_registry_enabled']).to be_present expect(json_response['container_registry_access_level']).to be_present expect(json_response['created_at']).to be_present + expect(json_response['updated_at']).to be_present expect(json_response['last_activity_at']).to be_present expect(json_response['shared_runners_enabled']).to be_present expect(json_response['group_runners_enabled']).to be_present @@ -2458,7 +2570,6 @@ RSpec.describe API::Projects, feature_category: :projects do expect(json_response['allow_merge_on_skipped_pipeline']).to eq(project.allow_merge_on_skipped_pipeline) expect(json_response['restrict_user_defined_variables']).to eq(project.restrict_user_defined_variables?) expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved) - expect(json_response['operations_access_level']).to be_present expect(json_response['security_and_compliance_access_level']).to be_present expect(json_response['releases_access_level']).to be_present expect(json_response['environments_access_level']).to be_present @@ -2470,19 +2581,19 @@ RSpec.describe API::Projects, feature_category: :projects do it 'exposes all necessary attributes' do create(:project_group_link, project: project) - get api("/projects/#{project.id}", admin) + get api(path, admin, admin_mode: true) diff = Set.new(json_response.keys) ^ Set.new(expected_keys) expect(diff).to be_empty, failure_message(diff) end - def failure_message(diff) + def failure_message(_diff) <<~MSG It looks like project's set of exposed attributes is different from the expected set. The following attributes are missing or newly added: - #{diff.to_a.to_sentence} + {diff.to_a.to_sentence} Please update #{project_attributes_file} file" MSG @@ -2496,11 +2607,11 @@ RSpec.describe API::Projects, feature_category: :projects do stub_container_registry_config(enabled: true, host_port: 'registry.example.org:5000') end - it 'returns a project by id', :aggregate_failures do + it 'returns a project by id' do group = create(:group) link = create(:project_group_link, project: project, group: group) - get api("/projects/#{project.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response['id']).to eq(project.id) @@ -2532,7 +2643,6 @@ RSpec.describe API::Projects, feature_category: :projects do expect(json_response['analytics_access_level']).to be_present expect(json_response['wiki_access_level']).to be_present expect(json_response['builds_access_level']).to be_present - expect(json_response['operations_access_level']).to be_present expect(json_response['security_and_compliance_access_level']).to be_present expect(json_response['releases_access_level']).to be_present expect(json_response['environments_access_level']).to be_present @@ -2584,7 +2694,7 @@ RSpec.describe API::Projects, feature_category: :projects do expires_at = 5.days.from_now.to_date link = create(:project_group_link, project: project, group: group, expires_at: expires_at) - get api("/projects/#{project.id}", user) + get api(path, user) expect(json_response['shared_with_groups']).to be_an Array expect(json_response['shared_with_groups'].length).to eq(1) @@ -2596,7 +2706,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'returns a project by path name' do - get api("/projects/#{project.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response['name']).to eq(project.name) end @@ -2609,7 +2719,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'returns a 404 error if user is not a member' do other_user = create(:user) - get api("/projects/#{project.id}", other_user) + get api(path, other_user) expect(response).to have_gitlab_http_status(:not_found) end @@ -2623,7 +2733,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'exposes namespace fields' do - get api("/projects/#{project.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response['namespace']).to eq({ @@ -2639,14 +2749,14 @@ RSpec.describe API::Projects, feature_category: :projects do end it "does not include license fields by default" do - get api("/projects/#{project.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to include('license', 'license_url') end it 'includes license fields when requested' do - get api("/projects/#{project.id}", user), params: { license: true } + get api(path, user), params: { license: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response['license']).to eq({ @@ -2659,14 +2769,14 @@ RSpec.describe API::Projects, feature_category: :projects do end it "does not include statistics by default" do - get api("/projects/#{project.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to include 'statistics' end it "includes statistics if requested" do - get api("/projects/#{project.id}", user), params: { statistics: true } + get api(path, user), params: { statistics: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response).to include 'statistics' @@ -2676,7 +2786,7 @@ RSpec.describe API::Projects, feature_category: :projects do let(:project) { create(:project, :public, :repository, :repository_private) } it "does not include statistics if user is not a member" do - get api("/projects/#{project.id}", user), params: { statistics: true } + get api(path, user), params: { statistics: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to include 'statistics' @@ -2685,7 +2795,7 @@ RSpec.describe API::Projects, feature_category: :projects do it "includes statistics if user is a member" do project.add_developer(user) - get api("/projects/#{project.id}", user), params: { statistics: true } + get api(path, user), params: { statistics: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response).to include 'statistics' @@ -2695,7 +2805,7 @@ RSpec.describe API::Projects, feature_category: :projects do project.add_developer(user) project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED) - get api("/projects/#{project.id}", user), params: { statistics: true } + get api(path, user), params: { statistics: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response).to include 'statistics' @@ -2703,14 +2813,14 @@ RSpec.describe API::Projects, feature_category: :projects do end it "includes import_error if user can admin project" do - get api("/projects/#{project.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to include("import_error") end it "does not include import_error if user cannot admin project" do - get api("/projects/#{project.id}", user3) + get api(path, user3) expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to include("import_error") @@ -2719,7 +2829,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'returns 404 when project is marked for deletion' do project.update!(pending_delete: true) - get api("/projects/#{project.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Project Not Found') @@ -2727,7 +2837,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'links exposure' do it 'exposes related resources full URIs' do - get api("/projects/#{project.id}", user) + get api(path, user) links = json_response['_links'] @@ -2801,7 +2911,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'personal project' do it 'sets project access and returns 200' do project.add_maintainer(user) - get api("/projects/#{project.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response['permissions']['project_access']['access_level']) @@ -2868,7 +2978,7 @@ RSpec.describe API::Projects, feature_category: :projects do let!(:project_member) { create(:project_member, :developer, user: user, project: project) } it 'returns group web_url and avatar_url' do - get api("/projects/#{project.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) @@ -2883,7 +2993,7 @@ RSpec.describe API::Projects, feature_category: :projects do let(:project) { create(:project, namespace: user.namespace) } it 'returns user web_url and avatar_url' do - get api("/projects/#{project.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) @@ -2894,21 +3004,61 @@ RSpec.describe API::Projects, feature_category: :projects do end end + context 'when authenticated as a developer' do + before do + project + project_member + end + + it 'hides sensitive admin attributes' do + get api(path, user3) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['id']).to eq(project.id) + expect(json_response['description']).to eq(project.description) + expect(json_response['default_branch']).to eq(project.default_branch) + expect(json_response['ci_config_path']).to eq(project.ci_config_path) + expect(json_response['forked_from_project']).to eq(project.forked_from_project) + expect(json_response['service_desk_address']).to eq(project.service_desk_address) + expect(json_response).not_to include( + 'ci_default_git_depth', + 'ci_forward_deployment_enabled', + 'ci_job_token_scope_enabled', + 'ci_separated_caches', + 'ci_allow_fork_pipelines_to_run_in_parent_project', + 'build_git_strategy', + 'keep_latest_artifact', + 'restrict_user_defined_variables', + 'runners_token', + 'runner_token_expiration_interval', + 'group_runners_enabled', + 'auto_cancel_pending_pipelines', + 'build_timeout', + 'auto_devops_enabled', + 'auto_devops_deploy_strategy', + 'import_error' + ) + end + end + 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 } } - subject { get api("/projects/#{project.id}", user) } + subject { get api(path, user) } end describe 'repository_storage attribute' do + let_it_be(:admin_mode) { false } + before do - get api("/projects/#{project.id}", user) + get api(path, user, admin_mode: admin_mode) end context 'when authenticated as an admin' do let(:user) { create(:admin) } + let_it_be(:admin_mode) { true } it 'returns repository_storage attribute' do expect(response).to have_gitlab_http_status(:ok) @@ -2924,31 +3074,34 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'exposes service desk attributes' do - get api("/projects/#{project.id}", user) + get api(path, user) expect(json_response).to have_key 'service_desk_enabled' expect(json_response).to have_key 'service_desk_address' end context 'when project is shared to multiple groups' do - it 'avoids N+1 queries' do + it 'avoids N+1 queries', :use_sql_query_cache do create(:project_group_link, project: project) - get api("/projects/#{project.id}", user) + get api(path, user) + expect(response).to have_gitlab_http_status(:ok) control = ActiveRecord::QueryRecorder.new do - get api("/projects/#{project.id}", user) + get api(path, user) end create(:project_group_link, project: project) expect do - get api("/projects/#{project.id}", user) + get api(path, user) end.not_to exceed_query_limit(control) end end end describe 'GET /projects/:id/users' do + let(:path) { "/projects/#{project.id}/users" } + shared_examples_for 'project users response' do let(:reporter_1) { create(:user) } let(:reporter_2) { create(:user) } @@ -2959,7 +3112,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'returns the project users' do - get api("/projects/#{project.id}/users", current_user) + get api(path, current_user) user = project.namespace.first_owner @@ -2978,6 +3131,10 @@ RSpec.describe API::Projects, feature_category: :projects do end end + it_behaves_like 'GET request permissions for admin mode' do + let(:failed_status_code) { :not_found } + end + context 'when unauthenticated' do it_behaves_like 'project users response' do let(:project) { create(:project, :public) } @@ -3003,7 +3160,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'returns a 404 error if user is not a member' do other_user = create(:user) - get api("/projects/#{project.id}/users", other_user) + get api(path, other_user) expect(response).to have_gitlab_http_status(:not_found) end @@ -3022,18 +3179,25 @@ RSpec.describe API::Projects, feature_category: :projects do end describe 'fork management' do - let(:project_fork_target) { create(:project) } - let(:project_fork_source) { create(:project, :public) } - let(:private_project_fork_source) { create(:project, :private) } + let_it_be_with_refind(:project_fork_target) { create(:project) } + let_it_be_with_refind(:project_fork_source) { create(:project, :public) } + let_it_be_with_refind(:private_project_fork_source) { create(:project, :private) } describe 'POST /projects/:id/fork/:forked_from_id' do + let(:path) { "/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}" } + + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { {} } + let(:failed_status_code) { :not_found } + end + context 'user is a developer' do before do project_fork_target.add_developer(user) end it 'denies project to be forked from an existing project' do - post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user) + post api(path, user) expect(response).to have_gitlab_http_status(:forbidden) end @@ -3051,7 +3215,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'allows project to be forked from an existing project' do expect(project_fork_target).not_to be_forked - post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user) + post api(path, user) project_fork_target.reload expect(response).to have_gitlab_http_status(:created) @@ -3063,7 +3227,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'fails without permission from forked_from project' do project_fork_source.project_feature.update_attribute(:forking_access_level, ProjectFeature::PRIVATE) - post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user) + post api(path, user) expect(response).to have_gitlab_http_status(:forbidden) expect(project_fork_target.forked_from_project).to be_nil @@ -3082,25 +3246,25 @@ RSpec.describe API::Projects, feature_category: :projects do it 'allows project to be forked from an existing project' do expect(project_fork_target).not_to be_forked - post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) + post api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:created) end it 'allows project to be forked from a private project' do - post api("/projects/#{project_fork_target.id}/fork/#{private_project_fork_source.id}", admin) + post api("/projects/#{project_fork_target.id}/fork/#{private_project_fork_source.id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:created) end it 'refreshes the forks count cachce' do expect do - post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) + post api(path, admin, admin_mode: true) end.to change(project_fork_source, :forks_count).by(1) end it 'fails if forked_from project which does not exist' do - post api("/projects/#{project_fork_target.id}/fork/#{non_existing_record_id}", admin) + post api("/projects/#{project_fork_target.id}/fork/#{non_existing_record_id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end @@ -3109,7 +3273,7 @@ RSpec.describe API::Projects, feature_category: :projects do Projects::ForkService.new(project_fork_source, admin).execute(project_fork_target) - post api("/projects/#{project_fork_target.id}/fork/#{other_project_fork_source.id}", admin) + post api("/projects/#{project_fork_target.id}/fork/#{other_project_fork_source.id}", admin, admin_mode: true) project_fork_target.reload expect(response).to have_gitlab_http_status(:conflict) @@ -3120,8 +3284,10 @@ RSpec.describe API::Projects, feature_category: :projects do end describe 'DELETE /projects/:id/fork' do + let(:path) { "/projects/#{project_fork_target.id}/fork" } + it "is not visible to users outside group" do - delete api("/projects/#{project_fork_target.id}/fork", user) + delete api(path, user) expect(response).to have_gitlab_http_status(:not_found) end @@ -3135,14 +3301,19 @@ RSpec.describe API::Projects, feature_category: :projects do context 'for a forked project' do before do - post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) + post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin, admin_mode: true) project_fork_target.reload expect(project_fork_target.forked_from_project).to be_present expect(project_fork_target).to be_forked end + it_behaves_like 'DELETE request permissions for admin mode' do + let(:success_status_code) { :no_content } + let(:failed_status_code) { :not_found } + end + it 'makes forked project unforked' do - delete api("/projects/#{project_fork_target.id}/fork", admin) + delete api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:no_content) project_fork_target.reload @@ -3151,18 +3322,18 @@ RSpec.describe API::Projects, feature_category: :projects do end it_behaves_like '412 response' do - let(:request) { api("/projects/#{project_fork_target.id}/fork", admin) } + subject(:request) { api(path, admin, admin_mode: true) } end end it 'is forbidden to non-owner users' do - delete api("/projects/#{project_fork_target.id}/fork", user2) + delete api(path, user2) expect(response).to have_gitlab_http_status(:forbidden) end it 'is idempotent if not forked' do expect(project_fork_target.forked_from_project).to be_nil - delete api("/projects/#{project_fork_target.id}/fork", admin) + delete api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_modified) expect(project_fork_target.reload.forked_from_project).to be_nil end @@ -3170,17 +3341,17 @@ RSpec.describe API::Projects, feature_category: :projects do end describe 'GET /projects/:id/forks' do - let(:private_fork) { create(:project, :private, :empty_repo) } - let(:member) { create(:user) } - let(:non_member) { create(:user) } + let_it_be_with_refind(:private_fork) { create(:project, :private, :empty_repo) } + let_it_be(:member) { create(:user) } + let_it_be(:non_member) { create(:user) } - before do + before_all do private_fork.add_developer(member) end context 'for a forked project' do before do - post api("/projects/#{private_fork.id}/fork/#{project_fork_source.id}", admin) + post api("/projects/#{private_fork.id}/fork/#{project_fork_source.id}", admin, admin_mode: true) private_fork.reload expect(private_fork.forked_from_project).to be_present expect(private_fork).to be_forked @@ -3198,6 +3369,20 @@ RSpec.describe API::Projects, feature_category: :projects do expect(json_response.length).to eq(1) expect(json_response[0]['name']).to eq(private_fork.name) end + + context 'filter by updated_at' do + before do + private_fork.update!(updated_at: 4.days.ago) + end + + it 'returns only forks updated on the given timeframe' do + get api("/projects/#{project_fork_source.id}/forks", member), + params: { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.map { |project| project['id'] }).to contain_exactly(private_fork.id) + end + end end context 'for a user that cannot access the forks' do @@ -3226,6 +3411,7 @@ RSpec.describe API::Projects, feature_category: :projects do describe "POST /projects/:id/share" do let_it_be(:group) { create(:group, :private) } let_it_be(:group_user) { create(:user) } + let(:path) { "/projects/#{project.id}/share" } before do group.add_developer(user) @@ -3236,7 +3422,7 @@ RSpec.describe API::Projects, feature_category: :projects do expires_at = 10.days.from_now.to_date expect do - post api("/projects/#{project.id}/share", user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at } + post api(path, user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at } end.to change { ProjectGroupLink.count }.by(1) expect(response).to have_gitlab_http_status(:created) @@ -3247,51 +3433,51 @@ RSpec.describe API::Projects, feature_category: :projects do it 'updates project authorization', :sidekiq_inline do expect do - post api("/projects/#{project.id}/share", user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER } + post api(path, user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER } end.to( change { group_user.can?(:read_project, project) }.from(false).to(true) ) end it "returns a 400 error when group id is not given" do - post api("/projects/#{project.id}/share", user), params: { group_access: Gitlab::Access::DEVELOPER } + post api(path, user), params: { group_access: Gitlab::Access::DEVELOPER } expect(response).to have_gitlab_http_status(:bad_request) end it "returns a 400 error when access level is not given" do - post api("/projects/#{project.id}/share", user), params: { group_id: group.id } + post api(path, user), params: { group_id: group.id } expect(response).to have_gitlab_http_status(:bad_request) end it "returns a 400 error when sharing is disabled" do project.namespace.update!(share_with_group_lock: true) - post api("/projects/#{project.id}/share", user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER } + post api(path, user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER } expect(response).to have_gitlab_http_status(:bad_request) end it 'returns a 404 error when user cannot read group' do private_group = create(:group, :private) - post api("/projects/#{project.id}/share", user), params: { group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER } + post api(path, user), params: { group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER } expect(response).to have_gitlab_http_status(:not_found) end it 'returns a 404 error when group does not exist' do - post api("/projects/#{project.id}/share", user), params: { group_id: non_existing_record_id, group_access: Gitlab::Access::DEVELOPER } + post api(path, user), params: { group_id: non_existing_record_id, group_access: Gitlab::Access::DEVELOPER } expect(response).to have_gitlab_http_status(:not_found) end it "returns a 400 error when wrong params passed" do - post api("/projects/#{project.id}/share", user), params: { group_id: group.id, group_access: non_existing_record_access_level } + post api(path, user), params: { group_id: group.id, group_access: non_existing_record_access_level } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq 'group_access does not have a valid value' end it "returns a 400 error when the project-group share is created with an OWNER access level" do - post api("/projects/#{project.id}/share", user), params: { group_id: group.id, group_access: Gitlab::Access::OWNER } + post api(path, user), params: { group_id: group.id, group_access: Gitlab::Access::OWNER } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq 'group_access does not have a valid value' @@ -3301,10 +3487,22 @@ RSpec.describe API::Projects, feature_category: :projects do allow(::Projects::GroupLinks::CreateService).to receive_message_chain(:new, :execute) .and_return({ status: :error, http_status: 409, message: 'error' }) - post api("/projects/#{project.id}/share", user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER } + post api(path, user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER } expect(response).to have_gitlab_http_status(:conflict) end + + context 'when project is forked' do + let(:forked_project) { fork_project(project) } + let(:path) { "/projects/#{forked_project.id}/share" } + + it 'returns a 404 error when group does not exist' do + forked_project.add_maintainer(user) + post api(path, user), params: { group_id: non_existing_record_id, group_access: Gitlab::Access::DEVELOPER } + + expect(response).to have_gitlab_http_status(:not_found) + end + end end describe 'DELETE /projects/:id/share/:group_id' do @@ -3334,7 +3532,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it_behaves_like '412 response' do - let(:request) { api("/projects/#{project.id}/share/#{group.id}", user) } + subject(:request) { api("/projects/#{project.id}/share/#{group.id}", user) } end end @@ -3360,6 +3558,7 @@ RSpec.describe API::Projects, feature_category: :projects do describe 'POST /projects/:id/import_project_members/:project_id' do let_it_be(:project2) { create(:project) } let_it_be(:project2_user) { create(:user) } + let(:path) { "/projects/#{project.id}/import_project_members/#{project2.id}" } before_all do project.add_maintainer(user) @@ -3368,7 +3567,8 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'records the query', :request_store, :use_sql_query_cache do - post api("/projects/#{project.id}/import_project_members/#{project2.id}", user) + post api(path, user) + expect(response).to have_gitlab_http_status(:created) control_project = create(:project) control_project.add_maintainer(user) @@ -3392,7 +3592,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'returns 200 when it successfully imports members from another project' do expect do - post api("/projects/#{project.id}/import_project_members/#{project2.id}", user) + post api(path, user) end.to change { project.members.count }.by(2) expect(response).to have_gitlab_http_status(:created) @@ -3435,7 +3635,7 @@ RSpec.describe API::Projects, feature_category: :projects do project2.add_developer(user2) expect do - post api("/projects/#{project.id}/import_project_members/#{project2.id}", user2) + post api(path, user2) end.not_to change { project.members.count } expect(response).to have_gitlab_http_status(:forbidden) @@ -3448,7 +3648,7 @@ RSpec.describe API::Projects, feature_category: :projects do end expect do - post api("/projects/#{project.id}/import_project_members/#{project2.id}", user) + post api(path, user) end.not_to change { project.members.count } expect(response).to have_gitlab_http_status(:unprocessable_entity) @@ -3457,6 +3657,8 @@ RSpec.describe API::Projects, feature_category: :projects do end describe 'PUT /projects/:id' do + let(:path) { "/projects/#{project.id}" } + before do expect(project).to be_persisted expect(user).to be_persisted @@ -3468,13 +3670,18 @@ RSpec.describe API::Projects, feature_category: :projects do expect(project_member).to be_persisted end + it_behaves_like 'PUT request permissions for admin mode' do + let(:params) { { visibility: 'internal' } } + let(:failed_status_code) { :not_found } + end + describe 'updating packages_enabled attribute' do it 'is enabled by default' do expect(project.packages_enabled).to be true end it 'disables project packages feature' do - put(api("/projects/#{project.id}", user), params: { packages_enabled: false }) + put(api(path, user), params: { packages_enabled: false }) expect(response).to have_gitlab_http_status(:ok) expect(project.reload.packages_enabled).to be false @@ -3482,8 +3689,8 @@ RSpec.describe API::Projects, feature_category: :projects do end end - it 'sets container_registry_access_level', :aggregate_failures do - put api("/projects/#{project.id}", user), params: { container_registry_access_level: 'private' } + it 'sets container_registry_access_level' do + put api(path, user), params: { container_registry_access_level: 'private' } expect(response).to have_gitlab_http_status(:ok) expect(json_response['container_registry_access_level']).to eq('private') @@ -3493,31 +3700,23 @@ RSpec.describe API::Projects, feature_category: :projects do it 'sets container_registry_enabled' do project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED) - put(api("/projects/#{project.id}", user), params: { container_registry_enabled: true }) + put(api(path, user), params: { container_registry_enabled: true }) expect(response).to have_gitlab_http_status(:ok) expect(json_response['container_registry_enabled']).to eq(true) expect(project.reload.container_registry_access_level).to eq(ProjectFeature::ENABLED) end - it 'sets security_and_compliance_access_level', :aggregate_failures do - put api("/projects/#{project.id}", user), params: { security_and_compliance_access_level: 'private' } + it 'sets security_and_compliance_access_level' do + put api(path, user), params: { security_and_compliance_access_level: 'private' } expect(response).to have_gitlab_http_status(:ok) expect(json_response['security_and_compliance_access_level']).to eq('private') expect(Project.find_by(path: project[:path]).security_and_compliance_access_level).to eq(ProjectFeature::PRIVATE) end - it 'sets operations_access_level', :aggregate_failures do - put api("/projects/#{project.id}", user), params: { operations_access_level: 'private' } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['operations_access_level']).to eq('private') - expect(Project.find_by(path: project[:path]).operations_access_level).to eq(ProjectFeature::PRIVATE) - end - - it 'sets analytics_access_level', :aggregate_failures do - put api("/projects/#{project.id}", user), params: { analytics_access_level: 'private' } + it 'sets analytics_access_level' do + put api(path, user), params: { analytics_access_level: 'private' } expect(response).to have_gitlab_http_status(:ok) expect(json_response['analytics_access_level']).to eq('private') @@ -3525,8 +3724,8 @@ RSpec.describe API::Projects, feature_category: :projects do end %i(releases_access_level environments_access_level feature_flags_access_level infrastructure_access_level monitor_access_level).each do |field| - it "sets #{field}", :aggregate_failures do - put api("/projects/#{project.id}", user), params: { field => 'private' } + it "sets #{field}" do + put api(path, user), params: { field => 'private' } expect(response).to have_gitlab_http_status(:ok) expect(json_response[field.to_s]).to eq('private') @@ -3537,7 +3736,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'returns 400 when nothing sent' do project_param = {} - put api("/projects/#{project.id}", user), params: project_param + put api(path, user), params: project_param expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to match('at least one parameter must be provided') @@ -3547,7 +3746,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'returns authentication error' do project_param = { name: 'bar' } - put api("/projects/#{project.id}"), params: project_param + put api(path), params: project_param expect(response).to have_gitlab_http_status(:unauthorized) end @@ -3593,7 +3792,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'does not update name to existing name' do project_param = { name: project3.name } - put api("/projects/#{project.id}", user), params: project_param + put api(path, user), params: project_param expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']['name']).to eq(['has already been taken']) @@ -3602,7 +3801,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'updates request_access_enabled' do project_param = { request_access_enabled: false } - put api("/projects/#{project.id}", user), params: project_param + put api(path, user), params: project_param expect(response).to have_gitlab_http_status(:ok) expect(json_response['request_access_enabled']).to eq(false) @@ -3623,7 +3822,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'updates default_branch' do project_param = { default_branch: 'something_else' } - put api("/projects/#{project.id}", user), params: project_param + put api(path, user), params: project_param expect(response).to have_gitlab_http_status(:ok) @@ -3712,7 +3911,7 @@ RSpec.describe API::Projects, feature_category: :projects do expect(response).to have_gitlab_http_status(:bad_request) end - it 'updates restrict_user_defined_variables', :aggregate_failures do + it 'updates restrict_user_defined_variables' do project_param = { restrict_user_defined_variables: true } put api("/projects/#{project3.id}", user), params: project_param @@ -3914,7 +4113,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'updates name' do project_param = { name: 'bar' } - put api("/projects/#{project.id}", user), params: project_param + put api(path, user), params: project_param expect(response).to have_gitlab_http_status(:ok) @@ -3989,7 +4188,7 @@ RSpec.describe API::Projects, feature_category: :projects do merge_requests_enabled: true, description: 'new description', request_access_enabled: true } - put api("/projects/#{project.id}", user3), params: project_param + put api(path, user3), params: project_param expect(response).to have_gitlab_http_status(:forbidden) end end @@ -4000,7 +4199,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'ignores visibility level restrictions' do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) - put api("/projects/#{project3.id}", admin), params: { visibility: 'internal' } + put api("/projects/#{project3.id}", admin, admin_mode: true), params: { visibility: 'internal' } expect(response).to have_gitlab_http_status(:ok) expect(json_response['visibility']).to eq('internal') @@ -4031,7 +4230,7 @@ RSpec.describe API::Projects, feature_category: :projects do let(:admin) { create(:admin) } it 'returns 400 when repository storage is unknown' do - put(api("/projects/#{new_project.id}", admin), params: { repository_storage: unknown_storage }) + put(api("/projects/#{new_project.id}", admin, admin_mode: true), params: { repository_storage: unknown_storage }) expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']['repository_storage_moves']).to eq(['is invalid']) @@ -4042,7 +4241,7 @@ RSpec.describe API::Projects, feature_category: :projects do expect do Sidekiq::Testing.fake! do - put(api("/projects/#{new_project.id}", admin), params: { repository_storage: 'test_second_storage' }) + put(api("/projects/#{new_project.id}", admin, admin_mode: true), params: { repository_storage: 'test_second_storage' }) end end.to change(Projects::UpdateRepositoryStorageWorker.jobs, :size).by(1) @@ -4052,40 +4251,42 @@ RSpec.describe API::Projects, feature_category: :projects do end context 'when updating service desk' do - subject { put(api("/projects/#{project.id}", user), params: { service_desk_enabled: true }) } + let(:params) { { service_desk_enabled: true } } + + subject(:request) { put(api(path, user), params: params) } before do project.update!(service_desk_enabled: false) - allow(::Gitlab::IncomingEmail).to receive(:enabled?).and_return(true) + allow(::Gitlab::Email::IncomingEmail).to receive(:enabled?).and_return(true) end it 'returns 200' do - subject + request expect(response).to have_gitlab_http_status(:ok) end it 'enables the service_desk' do - expect { subject }.to change { project.reload.service_desk_enabled }.to(true) + expect { request }.to change { project.reload.service_desk_enabled }.to(true) end end context 'when updating keep latest artifact' do - subject { put(api("/projects/#{project.id}", user), params: { keep_latest_artifact: true }) } + subject(:request) { put(api(path, user), params: { keep_latest_artifact: true }) } before do project.update!(keep_latest_artifact: false) end it 'returns 200' do - subject + request expect(response).to have_gitlab_http_status(:ok) end it 'enables keep_latest_artifact' do - expect { subject }.to change { project.reload.keep_latest_artifact }.to(true) + expect { request }.to change { project.reload.keep_latest_artifact }.to(true) end end @@ -4131,9 +4332,11 @@ RSpec.describe API::Projects, feature_category: :projects do end describe 'POST /projects/:id/archive' do + let(:path) { "/projects/#{project.id}/archive" } + context 'on an unarchived project' do it 'archives the project' do - post api("/projects/#{project.id}/archive", user) + post api(path, user) expect(response).to have_gitlab_http_status(:created) expect(json_response['archived']).to be_truthy @@ -4146,7 +4349,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'remains archived' do - post api("/projects/#{project.id}/archive", user) + post api(path, user) expect(response).to have_gitlab_http_status(:created) expect(json_response['archived']).to be_truthy @@ -4159,7 +4362,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'rejects the action' do - post api("/projects/#{project.id}/archive", user3) + post api(path, user3) expect(response).to have_gitlab_http_status(:forbidden) end @@ -4167,9 +4370,11 @@ RSpec.describe API::Projects, feature_category: :projects do end describe 'POST /projects/:id/unarchive' do + let(:path) { "/projects/#{project.id}/unarchive" } + context 'on an unarchived project' do it 'remains unarchived' do - post api("/projects/#{project.id}/unarchive", user) + post api(path, user) expect(response).to have_gitlab_http_status(:created) expect(json_response['archived']).to be_falsey @@ -4182,7 +4387,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'unarchives the project' do - post api("/projects/#{project.id}/unarchive", user) + post api(path, user) expect(response).to have_gitlab_http_status(:created) expect(json_response['archived']).to be_falsey @@ -4195,7 +4400,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'rejects the action' do - post api("/projects/#{project.id}/unarchive", user3) + post api(path, user3) expect(response).to have_gitlab_http_status(:forbidden) end @@ -4203,9 +4408,11 @@ RSpec.describe API::Projects, feature_category: :projects do end describe 'POST /projects/:id/star' do + let(:path) { "/projects/#{project.id}/star" } + context 'on an unstarred project' do it 'stars the project' do - expect { post api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(1) + expect { post api(path, user) }.to change { project.reload.star_count }.by(1) expect(response).to have_gitlab_http_status(:created) expect(json_response['star_count']).to eq(1) @@ -4219,7 +4426,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'does not modify the star count' do - expect { post api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } + expect { post api(path, user) }.not_to change { project.reload.star_count } expect(response).to have_gitlab_http_status(:not_modified) end @@ -4227,6 +4434,8 @@ RSpec.describe API::Projects, feature_category: :projects do end describe 'POST /projects/:id/unstar' do + let(:path) { "/projects/#{project.id}/unstar" } + context 'on a starred project' do before do user.toggle_star(project) @@ -4234,7 +4443,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'unstars the project' do - expect { post api("/projects/#{project.id}/unstar", user) }.to change { project.reload.star_count }.by(-1) + expect { post api(path, user) }.to change { project.reload.star_count }.by(-1) expect(response).to have_gitlab_http_status(:created) expect(json_response['star_count']).to eq(0) @@ -4243,7 +4452,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'on an unstarred project' do it 'does not modify the star count' do - expect { post api("/projects/#{project.id}/unstar", user) }.not_to change { project.reload.star_count } + expect { post api(path, user) }.not_to change { project.reload.star_count } expect(response).to have_gitlab_http_status(:not_modified) end @@ -4251,9 +4460,13 @@ RSpec.describe API::Projects, feature_category: :projects do end describe 'GET /projects/:id/starrers' do + let(:path) { "/projects/#{public_project.id}/starrers" } + let(:public_project) { create(:project, :public) } + let(:private_user) { create(:user, private_profile: true) } + shared_examples_for 'project starrers response' do it 'returns an array of starrers' do - get api("/projects/#{public_project.id}/starrers", current_user) + get api(path, current_user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -4263,15 +4476,12 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'returns the proper security headers' do - get api("/projects/#{public_project.id}/starrers", current_user) + get api(path, current_user) expect(response).to include_security_headers end end - let(:public_project) { create(:project, :public) } - let(:private_user) { create(:user, private_profile: true) } - before do user.update!(starred_projects: [public_project]) private_user.update!(starred_projects: [public_project]) @@ -4289,7 +4499,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'returns only starrers with a public profile' do - get api("/projects/#{public_project.id}/starrers", nil) + get api(path, nil) user_ids = json_response.map { |s| s['user']['id'] } expect(user_ids).to include(user.id) @@ -4303,7 +4513,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'returns current user with a private profile' do - get api("/projects/#{public_project.id}/starrers", private_user) + get api(path, private_user) user_ids = json_response.map { |s| s['user']['id'] } expect(user_ids).to include(user.id, private_user.id) @@ -4366,9 +4576,16 @@ RSpec.describe API::Projects, feature_category: :projects do end describe 'DELETE /projects/:id' do + let(:path) { "/projects/#{project.id}" } + + it_behaves_like 'DELETE request permissions for admin mode' do + let(:success_status_code) { :accepted } + let(:failed_status_code) { :not_found } + end + context 'when authenticated as user' do it 'removes project' do - delete api("/projects/#{project.id}", user) + delete api(path, user) expect(response).to have_gitlab_http_status(:accepted) expect(json_response['message']).to eql('202 Accepted') @@ -4376,13 +4593,13 @@ RSpec.describe API::Projects, feature_category: :projects do it_behaves_like '412 response' do let(:success_status) { 202 } - let(:request) { api("/projects/#{project.id}", user) } + subject(:request) { api(path, user) } end it 'does not remove a project if not an owner' do user3 = create(:user) project.add_developer(user3) - delete api("/projects/#{project.id}", user3) + delete api(path, user3) expect(response).to have_gitlab_http_status(:forbidden) end @@ -4392,27 +4609,27 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'does not remove a project not attached to user' do - delete api("/projects/#{project.id}", user2) + delete api(path, user2) expect(response).to have_gitlab_http_status(:not_found) end end context 'when authenticated as admin' do it 'removes any existing project' do - delete api("/projects/#{project.id}", admin) + delete api("/projects/#{project.id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:accepted) expect(json_response['message']).to eql('202 Accepted') end it 'does not remove a non existing project' do - delete api("/projects/#{non_existing_record_id}", admin) + delete api("/projects/#{non_existing_record_id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end it_behaves_like '412 response' do let(:success_status) { 202 } - let(:request) { api("/projects/#{project.id}", admin) } + subject(:request) { api("/projects/#{project.id}", admin, admin_mode: true) } end end end @@ -4422,6 +4639,8 @@ RSpec.describe API::Projects, feature_category: :projects do create(:project, :repository, creator: user, namespace: user.namespace) end + let(:path) { "/projects/#{project.id}/fork" } + let(:project2) do create(:project, :repository, creator: user, namespace: user.namespace) end @@ -4438,9 +4657,14 @@ RSpec.describe API::Projects, feature_category: :projects do project2.add_reporter(user2) end + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { {} } + let(:failed_status_code) { :not_found } + end + context 'when authenticated' do it 'forks if user has sufficient access to project' do - post api("/projects/#{project.id}/fork", user2) + post api(path, user2) expect(response).to have_gitlab_http_status(:created) expect(json_response['name']).to eq(project.name) @@ -4453,7 +4677,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'forks if user is admin' do - post api("/projects/#{project.id}/fork", admin) + post api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:created) expect(json_response['name']).to eq(project.name) @@ -4467,7 +4691,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'fails on missing project access for the project to fork' do new_user = create(:user) - post api("/projects/#{project.id}/fork", new_user) + post api(path, new_user) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Project Not Found') @@ -4492,41 +4716,41 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'forks with explicit own user namespace id' do - post api("/projects/#{project.id}/fork", user2), params: { namespace: user2.namespace.id } + post api(path, user2), params: { namespace: user2.namespace.id } expect(response).to have_gitlab_http_status(:created) expect(json_response['owner']['id']).to eq(user2.id) end it 'forks with explicit own user name as namespace' do - post api("/projects/#{project.id}/fork", user2), params: { namespace: user2.username } + post api(path, user2), params: { namespace: user2.username } expect(response).to have_gitlab_http_status(:created) expect(json_response['owner']['id']).to eq(user2.id) end it 'forks to another user when admin' do - post api("/projects/#{project.id}/fork", admin), params: { namespace: user2.username } + post api(path, admin, admin_mode: true), params: { namespace: user2.username } expect(response).to have_gitlab_http_status(:created) expect(json_response['owner']['id']).to eq(user2.id) end it 'fails if trying to fork to another user when not admin' do - post api("/projects/#{project.id}/fork", user2), params: { namespace: admin.namespace.id } + post api(path, user2), params: { namespace: admin.namespace.id } expect(response).to have_gitlab_http_status(:not_found) end it 'fails if trying to fork to non-existent namespace' do - post api("/projects/#{project.id}/fork", user2), params: { namespace: non_existing_record_id } + post api(path, user2), params: { namespace: non_existing_record_id } expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Namespace Not Found') end it 'forks to owned group' do - post api("/projects/#{project.id}/fork", user2), params: { namespace: group2.name } + post api(path, user2), params: { namespace: group2.name } expect(response).to have_gitlab_http_status(:created) expect(json_response['namespace']['name']).to eq(group2.name) @@ -4543,7 +4767,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'and namespace_id is specified alone' do before do - post api("/projects/#{project.id}/fork", user2), params: { namespace_id: user2.namespace.id } + post api(path, user2), params: { namespace_id: user2.namespace.id } end it_behaves_like 'forking to specified namespace_id' @@ -4551,7 +4775,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'and namespace_id and namespace are both specified' do before do - post api("/projects/#{project.id}/fork", user2), params: { namespace_id: user2.namespace.id, namespace: admin.namespace.id } + post api(path, user2), params: { namespace_id: user2.namespace.id, namespace: admin.namespace.id } end it_behaves_like 'forking to specified namespace_id' @@ -4559,7 +4783,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'and namespace_id and namespace_path are both specified' do before do - post api("/projects/#{project.id}/fork", user2), params: { namespace_id: user2.namespace.id, namespace_path: admin.namespace.path } + post api(path, user2), params: { namespace_id: user2.namespace.id, namespace_path: admin.namespace.path } end it_behaves_like 'forking to specified namespace_id' @@ -4577,7 +4801,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'and namespace_path is specified alone' do before do - post api("/projects/#{project.id}/fork", user2), params: { namespace_path: user2.namespace.path } + post api(path, user2), params: { namespace_path: user2.namespace.path } end it_behaves_like 'forking to specified namespace_path' @@ -4585,7 +4809,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'and namespace_path and namespace are both specified' do before do - post api("/projects/#{project.id}/fork", user2), params: { namespace_path: user2.namespace.path, namespace: admin.namespace.path } + post api(path, user2), params: { namespace_path: user2.namespace.path, namespace: admin.namespace.path } end it_behaves_like 'forking to specified namespace_path' @@ -4594,7 +4818,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'forks to owned subgroup' do full_path = "#{group2.path}/#{group3.path}" - post api("/projects/#{project.id}/fork", user2), params: { namespace: full_path } + post api(path, user2), params: { namespace: full_path } expect(response).to have_gitlab_http_status(:created) expect(json_response['namespace']['name']).to eq(group3.name) @@ -4602,21 +4826,21 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'fails to fork to not owned group' do - post api("/projects/#{project.id}/fork", user2), params: { namespace: group.name } + post api(path, user2), params: { namespace: group.name } expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq("404 Target Namespace Not Found") end it 'forks to not owned group when admin' do - post api("/projects/#{project.id}/fork", admin), params: { namespace: group.name } + post api(path, admin, admin_mode: true), params: { namespace: group.name } expect(response).to have_gitlab_http_status(:created) expect(json_response['namespace']['name']).to eq(group.name) end it 'accepts a path for the target project' do - post api("/projects/#{project.id}/fork", user2), params: { path: 'foobar' } + post api(path, user2), params: { path: 'foobar' } expect(response).to have_gitlab_http_status(:created) expect(json_response['name']).to eq(project.name) @@ -4629,7 +4853,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'fails to fork if path is already taken' do - post api("/projects/#{project.id}/fork", user2), params: { path: 'foobar' } + post api(path, user2), params: { path: 'foobar' } post api("/projects/#{project2.id}/fork", user2), params: { path: 'foobar' } expect(response).to have_gitlab_http_status(:conflict) @@ -4637,7 +4861,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'accepts custom parameters for the target project' do - post api("/projects/#{project.id}/fork", user2), + post api(path, user2), params: { name: 'My Random Project', description: 'A description', @@ -4659,7 +4883,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'fails to fork if name is already taken' do - post api("/projects/#{project.id}/fork", user2), params: { name: 'My Random Project' } + post api(path, user2), params: { name: 'My Random Project' } post api("/projects/#{project2.id}/fork", user2), params: { name: 'My Random Project' } expect(response).to have_gitlab_http_status(:conflict) @@ -4667,7 +4891,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'forks to the same namespace with alternative path and name' do - post api("/projects/#{project.id}/fork", user), params: { path: 'path_2', name: 'name_2' } + post api(path, user), params: { path: 'path_2', name: 'name_2' } expect(response).to have_gitlab_http_status(:created) expect(json_response['name']).to eq('name_2') @@ -4679,7 +4903,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'fails to fork to the same namespace without alternative path and name' do - post api("/projects/#{project.id}/fork", user) + post api(path, user) expect(response).to have_gitlab_http_status(:conflict) expect(json_response['message']['path']).to eq(['has already been taken']) @@ -4687,7 +4911,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'fails to fork with an unknown visibility level' do - post api("/projects/#{project.id}/fork", user2), params: { visibility: 'something' } + post api(path, user2), params: { visibility: 'something' } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq('visibility does not have a valid value') @@ -4696,7 +4920,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'when unauthenticated' do it 'returns authentication error' do - post api("/projects/#{project.id}/fork") + post api(path) expect(response).to have_gitlab_http_status(:unauthorized) expect(json_response['message']).to eq('401 Unauthorized') @@ -4710,7 +4934,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'denies project to be forked' do - post api("/projects/#{project.id}/fork", admin) + post api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end @@ -4720,8 +4944,9 @@ RSpec.describe API::Projects, feature_category: :projects do describe 'POST /projects/:id/housekeeping' do let(:housekeeping) { Repositories::HousekeepingService.new(project) } let(:params) { {} } + let(:path) { "/projects/#{project.id}/housekeeping" } - subject { post api("/projects/#{project.id}/housekeeping", user), params: params } + subject(:request) { post api(path, user), params: params } before do allow(Repositories::HousekeepingService).to receive(:new).with(project, :eager).and_return(housekeeping) @@ -4731,7 +4956,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'starts the housekeeping process' do expect(housekeeping).to receive(:execute).once - subject + request expect(response).to have_gitlab_http_status(:created) end @@ -4746,7 +4971,7 @@ RSpec.describe API::Projects, feature_category: :projects do message: "Housekeeping task: eager" )) - subject + request end context 'when requesting prune' do @@ -4756,7 +4981,7 @@ RSpec.describe API::Projects, feature_category: :projects do expect(Repositories::HousekeepingService).to receive(:new).with(project, :prune).and_return(housekeeping) expect(housekeeping).to receive(:execute).once - subject + request expect(response).to have_gitlab_http_status(:created) end @@ -4768,7 +4993,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'responds with bad_request' do expect(Repositories::HousekeepingService).not_to receive(:new) - subject + request expect(response).to have_gitlab_http_status(:bad_request) end @@ -4778,7 +5003,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'returns conflict' do expect(housekeeping).to receive(:execute).once.and_raise(Repositories::HousekeepingService::LeaseTaken) - subject + request expect(response).to have_gitlab_http_status(:conflict) expect(json_response['message']).to match(/Somebody already triggered housekeeping for this resource/) @@ -4792,7 +5017,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'returns forbidden error' do - post api("/projects/#{project.id}/housekeeping", user3) + post api(path, user3) expect(response).to have_gitlab_http_status(:forbidden) end @@ -4800,7 +5025,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'when unauthenticated' do it 'returns authentication error' do - post api("/projects/#{project.id}/housekeeping") + post api(path) expect(response).to have_gitlab_http_status(:unauthorized) end @@ -4809,6 +5034,7 @@ RSpec.describe API::Projects, feature_category: :projects do describe 'POST /projects/:id/repository_size' do let(:update_statistics_service) { Projects::UpdateStatisticsService.new(project, nil, statistics: [:repository_size, :lfs_objects_size]) } + let(:path) { "/projects/#{project.id}/repository_size" } before do allow(Projects::UpdateStatisticsService).to receive(:new).with(project, nil, statistics: [:repository_size, :lfs_objects_size]).and_return(update_statistics_service) @@ -4818,7 +5044,7 @@ RSpec.describe API::Projects, feature_category: :projects do it 'starts the housekeeping process' do expect(update_statistics_service).to receive(:execute).once - post api("/projects/#{project.id}/repository_size", user) + post api(path, user) expect(response).to have_gitlab_http_status(:created) end @@ -4830,7 +5056,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'returns forbidden error' do - post api("/projects/#{project.id}/repository_size", user3) + post api(path, user3) expect(response).to have_gitlab_http_status(:forbidden) end @@ -4838,7 +5064,7 @@ RSpec.describe API::Projects, feature_category: :projects do context 'when unauthenticated' do it 'returns authentication error' do - post api("/projects/#{project.id}/repository_size") + post api(path) expect(response).to have_gitlab_http_status(:unauthorized) end @@ -4846,31 +5072,33 @@ RSpec.describe API::Projects, feature_category: :projects do end describe 'PUT /projects/:id/transfer' do + let(:path) { "/projects/#{project.id}/transfer" } + context 'when authenticated as owner' do let(:group) { create :group } it 'transfers the project to the new namespace' do group.add_owner(user) - put api("/projects/#{project.id}/transfer", user), params: { namespace: group.id } + put api(path, user), params: { namespace: group.id } expect(response).to have_gitlab_http_status(:ok) end it 'fails when transferring to a non owned namespace' do - put api("/projects/#{project.id}/transfer", user), params: { namespace: group.id } + put api(path, user), params: { namespace: group.id } expect(response).to have_gitlab_http_status(:not_found) end it 'fails when transferring to an unknown namespace' do - put api("/projects/#{project.id}/transfer", user), params: { namespace: 'unknown' } + put api(path, user), params: { namespace: 'unknown' } expect(response).to have_gitlab_http_status(:not_found) end it 'fails on missing namespace' do - put api("/projects/#{project.id}/transfer", user) + put api(path, user) expect(response).to have_gitlab_http_status(:bad_request) end @@ -4885,7 +5113,7 @@ RSpec.describe API::Projects, feature_category: :projects do let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) } it 'fails transferring the project to the target namespace' do - put api("/projects/#{project.id}/transfer", user), params: { namespace: group.id } + put api(path, user), params: { namespace: group.id } expect(response).to have_gitlab_http_status(:bad_request) end @@ -4988,16 +5216,20 @@ RSpec.describe API::Projects, feature_category: :projects do end describe 'GET /projects/:id/storage' do + let(:path) { "/projects/#{project.id}/storage" } + + it_behaves_like 'GET request permissions for admin mode' + context 'when unauthenticated' do it 'does not return project storage data' do - get api("/projects/#{project.id}/storage") + get api(path) expect(response).to have_gitlab_http_status(:unauthorized) end end it 'returns project storage data when user is admin' do - get api("/projects/#{project.id}/storage", create(:admin)) + get api(path, create(:admin), admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['project_id']).to eq(project.id) @@ -5007,7 +5239,7 @@ RSpec.describe API::Projects, feature_category: :projects do end it 'does not return project storage data when user is not admin' do - get api("/projects/#{project.id}/storage", user3) + get api(path, user3) expect(response).to have_gitlab_http_status(:forbidden) end diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb index 8e8a25a8dc2..04d5f7ac20a 100644 --- a/spec/requests/api/protected_branches_spec.rb +++ b/spec/requests/api/protected_branches_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe API::ProtectedBranches, feature_category: :source_code_management do let_it_be_with_reload(:project) { create(:project, :repository) } let_it_be(:maintainer) { create(:user) } + let_it_be(:developer) { create(:user) } let_it_be(:guest) { create(:user) } let(:protected_name) { 'feature' } @@ -16,12 +17,14 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management before_all do project.add_maintainer(maintainer) + project.add_developer(developer) project.add_guest(guest) end describe "GET /projects/:id/protected_branches" do let(:params) { {} } let(:route) { "/projects/#{project.id}/protected_branches" } + let(:expected_branch_names) { project.protected_branches.map { |x| x['name'] } } shared_examples_for 'protected branches' do it 'returns the protected branches' do @@ -39,9 +42,7 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management let(:user) { maintainer } context 'when search param is not present' do - it_behaves_like 'protected branches' do - let(:expected_branch_names) { project.protected_branches.map { |x| x['name'] } } - end + it_behaves_like 'protected branches' end context 'when search param is present' do @@ -53,6 +54,12 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management end end + context 'when authenticated as a developer' do + let(:user) { developer } + + it_behaves_like 'protected branches' + end + context 'when authenticated as a guest' do let(:user) { guest } @@ -103,6 +110,27 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management it_behaves_like 'protected branch' end + + context 'when a deploy key is present' do + let(:deploy_key) do + create(:deploy_key, deploy_keys_projects: [create(:deploy_keys_project, :write_access, project: project)]) + end + + it 'returns deploy key information' do + create(:protected_branch_push_access_level, protected_branch: protected_branch, deploy_key: deploy_key) + get api(route, user) + + expect(json_response['push_access_levels']).to include( + a_hash_including('access_level_description' => 'Deploy key', 'deploy_key_id' => deploy_key.id) + ) + end + end + end + + context 'when authenticated as a developer' do + let(:user) { developer } + + it_behaves_like 'protected branch' end context 'when authenticated as a guest' do @@ -243,10 +271,20 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management end end + context 'when authenticated as a developer' do + let(:user) { developer } + + it "returns a 403 error" do + post post_endpoint, params: { name: branch_name } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + context 'when authenticated as a guest' do let(:user) { guest } - it "returns a 403 error if guest" do + it "returns a 403 error" do post post_endpoint, params: { name: branch_name } expect(response).to have_gitlab_http_status(:forbidden) @@ -266,6 +304,15 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management end.to change { protected_branch.reload.allow_force_push }.from(false).to(true) expect(response).to have_gitlab_http_status(:ok) end + + context 'when allow_force_push is not set' do + it 'responds with a bad request error' do + patch api(route, user), params: { allow_force_push: nil } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq 'allow_force_push is empty' + end + end end context 'when returned protected branch is invalid' do @@ -286,6 +333,16 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management end end + context 'when authenticated as a developer' do + let(:user) { developer } + + it "returns a 403 error" do + patch api(route, user), params: { allow_force_push: true } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + context 'when authenticated as a guest' do let(:user) { guest } @@ -298,42 +355,65 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management end describe "DELETE /projects/:id/protected_branches/unprotect/:branch" do - let(:user) { maintainer } let(:delete_endpoint) { api("/projects/#{project.id}/protected_branches/#{branch_name}", user) } - it "unprotects a single branch" do - delete delete_endpoint + context "when authenticated as a maintainer" do + let(:user) { maintainer } - expect(response).to have_gitlab_http_status(:no_content) - end + it "unprotects a single branch" do + delete delete_endpoint - it_behaves_like '412 response' do - let(:request) { delete_endpoint } - end + expect(response).to have_gitlab_http_status(:no_content) + end + + it_behaves_like '412 response' do + let(:request) { delete_endpoint } + end + + it "returns 404 if branch does not exist" do + delete api("/projects/#{project.id}/protected_branches/barfoo", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + context 'when a policy restricts rule deletion' do + it "prevents deletion of the protected branch rule" do + disallow(:destroy_protected_branch, protected_branch) - it "returns 404 if branch does not exist" do - delete api("/projects/#{project.id}/protected_branches/barfoo", user) + delete delete_endpoint - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when branch has a wildcard in its name' do + let(:protected_name) { 'feature*' } + + it "unprotects a wildcard branch" do + delete delete_endpoint + + expect(response).to have_gitlab_http_status(:no_content) + end + end end - context 'when a policy restricts rule deletion' do - it "prevents deletion of the protected branch rule" do - disallow(:destroy_protected_branch, protected_branch) + context 'when authenticated as a developer' do + let(:user) { developer } + it "returns a 403 error" do delete delete_endpoint expect(response).to have_gitlab_http_status(:forbidden) end end - context 'when branch has a wildcard in its name' do - let(:protected_name) { 'feature*' } + context 'when authenticated as a guest' do + let(:user) { guest } - it "unprotects a wildcard branch" do + it "returns a 403 error" do delete delete_endpoint - expect(response).to have_gitlab_http_status(:no_content) + expect(response).to have_gitlab_http_status(:forbidden) end end end diff --git a/spec/requests/api/protected_tags_spec.rb b/spec/requests/api/protected_tags_spec.rb index 5b128d4ec9e..c6398e624f8 100644 --- a/spec/requests/api/protected_tags_spec.rb +++ b/spec/requests/api/protected_tags_spec.rb @@ -84,6 +84,21 @@ RSpec.describe API::ProtectedTags, feature_category: :source_code_management do it_behaves_like 'protected tag' end + + context 'when a deploy key is present' do + let(:deploy_key) do + create(:deploy_key, deploy_keys_projects: [create(:deploy_keys_project, :write_access, project: project)]) + end + + it 'returns deploy key information' do + create(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: deploy_key) + get api(route, user) + + expect(json_response['create_access_levels']).to include( + a_hash_including('access_level_description' => 'Deploy key', 'deploy_key_id' => deploy_key.id) + ) + end + end end context 'when authenticated as a guest' do diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb index 978d4f72a4a..0b2641b062c 100644 --- a/spec/requests/api/pypi_packages_spec.rb +++ b/spec/requests/api/pypi_packages_spec.rb @@ -14,10 +14,18 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } let_it_be(:job) { create(:ci_build, :running, user: user, project: project) } - let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_pypi_user' } } + let(:snowplow_gitlab_standard_context) { snowplow_context } let(:headers) { {} } + def snowplow_context(user_role: :developer) + if user_role == :anonymous + { project: project, namespace: project.namespace, property: 'i_package_pypi_user' } + else + { project: project, namespace: project.namespace, property: 'i_package_pypi_user', user: user } + end + end + context 'simple index API endpoint' do let_it_be(:package) { create(:pypi_package, project: project) } let_it_be(:package2) { create(:pypi_package, project: project) } @@ -26,7 +34,6 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do describe 'GET /api/v4/groups/:id/-/packages/pypi/simple' do let(:url) { "/groups/#{group.id}/-/packages/pypi/simple" } - let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_pypi_user' } } it_behaves_like 'pypi simple index API endpoint' it_behaves_like 'rejects PyPI access with unknown group id' @@ -82,13 +89,13 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do context 'simple package API endpoint' do let_it_be(:package) { create(:pypi_package, project: project) } - let(:snowplow_gitlab_standard_context) { { project: nil, namespace: group, property: 'i_package_pypi_user' } } subject { get api(url), headers: headers } describe 'GET /api/v4/groups/:id/-/packages/pypi/simple/:package_name' do let(:package_name) { package.name } let(:url) { "/groups/#{group.id}/-/packages/pypi/simple/#{package_name}" } + let(:snowplow_context) { { project: nil, namespace: project.namespace, property: 'i_package_pypi_user' } } it_behaves_like 'pypi simple API endpoint' it_behaves_like 'rejects PyPI access with unknown group id' @@ -126,7 +133,7 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do describe 'GET /api/v4/projects/:id/packages/pypi/simple/:package_name' do let(:package_name) { package.name } let(:url) { "/projects/#{project.id}/packages/pypi/simple/#{package_name}" } - let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_pypi_user' } } + let(:snowplow_context) { { project: project, namespace: project.namespace, property: 'i_package_pypi_user' } } it_behaves_like 'pypi simple API endpoint' it_behaves_like 'rejects PyPI access with unknown project id' @@ -242,6 +249,13 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do let(:token) { user_token ? personal_access_token.token : 'wrong' } let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } let(:headers) { user_headers.merge(workhorse_headers) } + let(:snowplow_gitlab_standard_context) do + if user_role == :anonymous || (visibility_level == :public && !user_token) + { project: project, namespace: project.namespace, property: 'i_package_pypi_user' } + else + { project: project, namespace: project.namespace, property: 'i_package_pypi_user', user: user } + end + end before do project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s)) @@ -379,6 +393,14 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do let_it_be(:package_name) { 'Dummy-Package' } let_it_be(:package) { create(:pypi_package, project: project, name: package_name, version: '1.0.0') } + let(:snowplow_gitlab_standard_context) do + if user_role == :anonymous || (visibility_level == :public && !user_token) + { project: project, namespace: project.namespace, property: 'i_package_pypi_user' } + else + { project: project, namespace: project.namespace, property: 'i_package_pypi_user', user: user } + end + end + subject { get api(url), headers: headers } describe 'GET /api/v4/groups/:id/-/packages/pypi/files/:sha256/*file_identifier' do diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb index 462cc1e3b5d..b8c10de2302 100644 --- a/spec/requests/api/release/links_spec.rb +++ b/spec/requests/api/release/links_spec.rb @@ -174,7 +174,7 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do specify do get api("/projects/#{project.id}/releases/v0.1/assets/links/#{link.id}", maintainer) - expect(json_response['direct_asset_url']).to eq("http://localhost/#{project.namespace.path}/#{project.name}/-/releases/#{release.tag}/downloads/bin/bigfile.exe") + expect(json_response['direct_asset_url']).to eq("http://localhost/#{project.full_path}/-/releases/#{release.tag}/downloads/bin/bigfile.exe") end end @@ -377,12 +377,21 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do expect(response).to match_response_schema('release/link') end + context 'when params are invalid' do + it 'returns 400 error' do + put api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}", maintainer), + params: params.merge(url: 'wrong_url') + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + context 'when using `direct_asset_path`' do it 'updates the release link' do put api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}", maintainer), params: params.merge(direct_asset_path: '/binaries/awesome-app.msi') - expect(json_response['direct_asset_url']).to eq("http://localhost/#{project.namespace.path}/#{project.name}/-/releases/#{release.tag}/downloads/binaries/awesome-app.msi") + expect(json_response['direct_asset_url']).to eq("http://localhost/#{project.full_path}/-/releases/#{release.tag}/downloads/binaries/awesome-app.msi") end end @@ -534,6 +543,21 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do end end + context 'when destroy process fails' do + before do + allow_next_instance_of(::Releases::Links::DestroyService) do |service| + allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'error')) + end + end + + it_behaves_like '400 response' do + let(:message) { 'error' } + let(:request) do + delete api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}", maintainer) + end + end + end + context 'when there are no corresponding release link' do let!(:release_link) {} diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index c3f99872cef..0b5cc3611bd 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Releases, feature_category: :release_orchestration do +RSpec.describe API::Releases, :aggregate_failures, feature_category: :release_orchestration do let(:project) { create(:project, :repository, :private) } let(:maintainer) { create(:user) } let(:reporter) { create(:user) } @@ -422,22 +422,6 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do .to eq('release-18.04.dmg') expect(json_response['assets']['links'].first['url']) .to eq('https://my-external-hosting.example.com/scrambled-url/app.zip') - expect(json_response['assets']['links'].first['external']) - .to be_truthy - end - - context 'when link is internal' do - let(:url) do - "#{project.web_url}/-/jobs/artifacts/v11.6.0-rc4/download?" \ - "job=rspec-mysql+41%2F50" - end - - it 'has external false' do - get api("/projects/#{project.id}/releases/v0.1", maintainer) - - expect(json_response['assets']['links'].first['external']) - .to be_falsy - end end end @@ -480,7 +464,7 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do end context 'when specified tag is not found in the project' do - it 'returns 404 for maintater' do + it 'returns 404 for maintainer' do get api("/projects/#{project.id}/releases/non_exist_tag", maintainer) expect(response).to have_gitlab_http_status(:not_found) @@ -1665,7 +1649,11 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do let_it_be(:release2) { create(:release, project: project2) } let_it_be(:release3) { create(:release, project: project3) } - context 'when authenticated as owner' do + it_behaves_like 'GET request permissions for admin mode' do + let(:path) { "/groups/#{group1.id}/releases" } + end + + context 'when authenticated as owner', :enable_admin_mode do it 'gets releases from all projects in the group' do get api("/groups/#{group1.id}/releases", admin) @@ -1715,9 +1703,14 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do context 'with subgroups' do let(:group) { create(:group) } - it 'include_subgroups avoids N+1 queries' do + subject { get api("/groups/#{group.id}/releases", admin, admin_mode: true), params: query_params.merge({ include_subgroups: true }) } + + it 'include_subgroups avoids N+1 queries', :use_sql_query_cache do + subject + expect(response).to have_gitlab_http_status(:ok) + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do - get api("/groups/#{group.id}/releases", admin), params: query_params.merge({ include_subgroups: true }) + subject end.count subgroups = create_list(:group, 10, parent: group1) @@ -1725,7 +1718,7 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do create_list(:release, 10, project: projects[0], author: admin) expect do - get api("/groups/#{group.id}/releases", admin), params: query_params.merge({ include_subgroups: true }) + subject end.not_to exceed_all_query_limit(control_count) end end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index be26fe24061..8853eff0b3e 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -236,7 +236,6 @@ RSpec.describe API::Repositories, feature_category: :source_code_management do get api(route, current_user) expect(response.headers["Cache-Control"]).to eq("max-age=0, private, must-revalidate, no-store, no-cache") - expect(response.headers["Pragma"]).to eq("no-cache") expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT") end diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb index 6a89e9a56df..ce05fa2b383 100644 --- a/spec/requests/api/resource_access_tokens_spec.rb +++ b/spec/requests/api/resource_access_tokens_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -RSpec.describe API::ResourceAccessTokens, feature_category: :authentication_and_authorization do +RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do let_it_be(:user) { create(:user) } let_it_be(:user_non_priviledged) { create(:user) } @@ -336,13 +336,33 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :authentication_and_ context "when 'expires_at' is not set" do let(:expires_at) { nil } - it "creates a #{source_type} access token with the params", :aggregate_failures do - create_token + context 'when default_pat_expiration feature flag is true' do + it "creates a #{source_type} access token with the default expires_at value", :aggregate_failures do + freeze_time do + create_token + expires_at = PersonalAccessToken::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS.days.from_now + + expect(response).to have_gitlab_http_status(:created) + expect(json_response["name"]).to eq("test") + expect(json_response["scopes"]).to eq(["api"]) + expect(json_response["expires_at"]).to eq(expires_at.to_date.iso8601) + end + end + end - expect(response).to have_gitlab_http_status(:created) - expect(json_response["name"]).to eq("test") - expect(json_response["scopes"]).to eq(["api"]) - expect(json_response["expires_at"]).to eq(nil) + context 'when default_pat_expiration feature flag is false' do + before do + stub_feature_flags(default_pat_expiration: false) + end + + it "creates a #{source_type} access token with the params", :aggregate_failures do + create_token + + expect(response).to have_gitlab_http_status(:created) + expect(json_response["name"]).to eq("test") + expect(json_response["scopes"]).to eq(["api"]) + expect(json_response["expires_at"]).to eq(nil) + end end end @@ -468,10 +488,86 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :authentication_and_ end end end + + context "POST #{source_type}s/:id/access_tokens/:token_id/rotate" do + let_it_be(:project_bot) { create(:user, :project_bot) } + let_it_be(:token) { create(:personal_access_token, user: project_bot) } + let_it_be(:resource_id) { resource.id } + let_it_be(:token_id) { token.id } + + let(:path) { "/#{source_type}s/#{resource_id}/access_tokens/#{token_id}/rotate" } + + before do + resource.add_maintainer(project_bot) + resource.add_owner(user) + end + + subject(:rotate_token) { post api(path, user) } + + it "allows owner to rotate token", :freeze_time do + rotate_token + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['token']).not_to eq(token.token) + expect(json_response['expires_at']).to eq((Date.today + 1.week).to_s) + end + + context 'without permission' do + it 'returns an error message' do + another_user = create(:user) + resource.add_developer(another_user) + + post api(path, another_user) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'when service raises an error' do + let(:error_message) { 'boom!' } + + before do + allow_next_instance_of(PersonalAccessTokens::RotateService) do |service| + allow(service).to receive(:execute).and_return(ServiceResponse.error(message: error_message)) + end + end + + it 'returns the same error message' do + rotate_token + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq("400 Bad request - #{error_message}") + end + end + + context 'when token does not exist' do + let(:invalid_path) { "/#{source_type}s/#{resource_id}/access_tokens/#{non_existing_record_id}/rotate" } + + context 'for non-admin user' do + it 'returns unauthorized' do + user = create(:user) + resource.add_developer(user) + + post api(invalid_path, user) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'for admin user', :enable_admin_mode do + it 'returns not found' do + admin = create(:admin) + post api(invalid_path, admin) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end end context 'when the resource is a project' do - let_it_be(:resource) { create(:project) } + let_it_be(:resource) { create(:project, group: create(:group)) } let_it_be(:other_resource) { create(:project) } let_it_be(:unknown_resource) { create(:project) } diff --git a/spec/requests/api/rubygem_packages_spec.rb b/spec/requests/api/rubygem_packages_spec.rb index 34cf6033811..1774b43ccb3 100644 --- a/spec/requests/api/rubygem_packages_spec.rb +++ b/spec/requests/api/rubygem_packages_spec.rb @@ -8,6 +8,14 @@ RSpec.describe API::RubygemPackages, feature_category: :package_registry do using RSpec::Parameterized::TableSyntax let_it_be_with_reload(:project) { create(:project) } + let(:tokens) do + { + personal_access_token: personal_access_token.token, + deploy_token: deploy_token.token, + job_token: job.token + } + end + let_it_be(:personal_access_token) { create(:personal_access_token) } let_it_be(:user) { personal_access_token.user } let_it_be(:job) { create(:ci_build, :running, user: user, project: project) } @@ -15,14 +23,14 @@ RSpec.describe API::RubygemPackages, feature_category: :package_registry do let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } let_it_be(:headers) { {} } - let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_rubygems_user' } } + let(:snowplow_gitlab_standard_context) { snowplow_context } - let(:tokens) do - { - personal_access_token: personal_access_token.token, - deploy_token: deploy_token.token, - job_token: job.token - } + def snowplow_context(user_role: :developer) + if user_role == :anonymous + { project: project, namespace: project.namespace, property: 'i_package_rubygems_user' } + else + { project: project, namespace: project.namespace, property: 'i_package_rubygems_user', user: user } + end end shared_examples 'when feature flag is disabled' do @@ -164,7 +172,13 @@ RSpec.describe API::RubygemPackages, feature_category: :package_registry do with_them do let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' } let(:headers) { user_role == :anonymous ? {} : { 'HTTP_AUTHORIZATION' => token } } - let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_rubygems_user' } } + let(:snowplow_gitlab_standard_context) do + if token_type == :deploy_token + snowplow_context.merge(user: deploy_token) + else + snowplow_context(user_role: user_role) + end + end before do project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility.to_s)) diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index 035f53db12e..a315bca58d1 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Search, feature_category: :global_search do +RSpec.describe API::Search, :clean_gitlab_redis_rate_limiting, feature_category: :global_search do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } let_it_be(:project, reload: true) { create(:project, :wiki_repo, :public, name: 'awesome project', group: group) } @@ -10,8 +10,6 @@ RSpec.describe API::Search, feature_category: :global_search do before do allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).and_return(0) - allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000) - allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000) end shared_examples 'response is correct' do |schema:, size: 1| @@ -141,7 +139,7 @@ RSpec.describe API::Search, feature_category: :global_search do end end - context 'when DB timeouts occur from global searches', :aggregate_errors do + context 'when DB timeouts occur from global searches', :aggregate_failures do %w( issues merge_requests @@ -174,6 +172,23 @@ RSpec.describe API::Search, feature_category: :global_search do end end + context 'when there is a search error' do + let(:results) { instance_double('Gitlab::SearchResults', failed?: true, error: 'failed to parse query') } + + before do + allow_next_instance_of(SearchService) do |service| + allow(service).to receive(:search_objects).and_return([]) + allow(service).to receive(:search_results).and_return(results) + end + end + + it 'returns 400 error' do + get api(endpoint, user), params: { scope: 'issues', search: 'expected to fail' } + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + context 'with correct params' do context 'for projects scope' do before do diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 4d85849cff3..3f66cbaf2b7 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, feature_category: :not_owned do +RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, feature_category: :shared do let(:user) { create(:user) } let_it_be(:admin) { create(:admin) } @@ -66,6 +66,16 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['jira_connect_application_key']).to eq(nil) expect(json_response['jira_connect_proxy_url']).to eq(nil) expect(json_response['user_defaults_to_private_profile']).to eq(false) + expect(json_response['default_syntax_highlighting_theme']).to eq(1) + expect(json_response['projects_api_rate_limit_unauthenticated']).to eq(400) + expect(json_response['silent_mode_enabled']).to be(false) + expect(json_response['slack_app_enabled']).to be(false) + expect(json_response['slack_app_id']).to be_nil + expect(json_response['slack_app_secret']).to be_nil + expect(json_response['slack_app_signing_secret']).to be_nil + expect(json_response['slack_app_verification_token']).to be_nil + expect(json_response['valid_runner_registrars']).to match_array(%w(project group)) + expect(json_response['ci_max_includes']).to eq(150) end end @@ -169,7 +179,16 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu jira_connect_proxy_url: 'http://example.com', bulk_import_enabled: false, allow_runner_registration_token: true, - user_defaults_to_private_profile: true + user_defaults_to_private_profile: true, + default_syntax_highlighting_theme: 2, + projects_api_rate_limit_unauthenticated: 100, + silent_mode_enabled: true, + slack_app_enabled: true, + slack_app_id: 'SLACK_APP_ID', + slack_app_secret: 'SLACK_APP_SECRET', + slack_app_signing_secret: 'SLACK_APP_SIGNING_SECRET', + slack_app_verification_token: 'SLACK_APP_VERIFICATION_TOKEN', + valid_runner_registrars: ['group'] } expect(response).to have_gitlab_http_status(:ok) @@ -237,6 +256,15 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu expect(json_response['bulk_import_enabled']).to be(false) expect(json_response['allow_runner_registration_token']).to be(true) expect(json_response['user_defaults_to_private_profile']).to be(true) + expect(json_response['default_syntax_highlighting_theme']).to eq(2) + expect(json_response['projects_api_rate_limit_unauthenticated']).to be(100) + expect(json_response['silent_mode_enabled']).to be(true) + expect(json_response['slack_app_enabled']).to be(true) + expect(json_response['slack_app_id']).to eq('SLACK_APP_ID') + expect(json_response['slack_app_secret']).to eq('SLACK_APP_SECRET') + expect(json_response['slack_app_signing_secret']).to eq('SLACK_APP_SIGNING_SECRET') + expect(json_response['slack_app_verification_token']).to eq('SLACK_APP_VERIFICATION_TOKEN') + expect(json_response['valid_runner_registrars']).to eq(['group']) end end @@ -807,6 +835,40 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu end end + context 'with ci_max_includes' do + it 'updates the settings' do + put api("/application/settings", admin), params: { + ci_max_includes: 200 + } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + 'ci_max_includes' => 200 + ) + end + + it 'allows a zero value' do + put api("/application/settings", admin), params: { + ci_max_includes: 0 + } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + 'ci_max_includes' => 0 + ) + end + + it 'does not allow a nil value' do + put api("/application/settings", admin), params: { + ci_max_includes: nil + } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']['ci_max_includes']) + .to include(a_string_matching('is not a number')) + end + end + context 'with housekeeping enabled' do it 'at least one of housekeeping_incremental_repack_period or housekeeping_optimize_repository_period is required' do put api("/application/settings", admin), params: { diff --git a/spec/requests/api/sidekiq_metrics_spec.rb b/spec/requests/api/sidekiq_metrics_spec.rb index 1085df97cc7..1ac065f0c0c 100644 --- a/spec/requests/api/sidekiq_metrics_spec.rb +++ b/spec/requests/api/sidekiq_metrics_spec.rb @@ -2,12 +2,19 @@ require 'spec_helper' -RSpec.describe API::SidekiqMetrics, feature_category: :not_owned do +RSpec.describe API::SidekiqMetrics, :aggregate_failures, feature_category: :shared do let(:admin) { create(:user, :admin) } describe 'GET sidekiq/*' do + %w[/sidekiq/queue_metrics /sidekiq/process_metrics /sidekiq/job_stats + /sidekiq/compound_metrics].each do |path| + it_behaves_like 'GET request permissions for admin mode' do + let(:path) { path } + end + end + it 'defines the `queue_metrics` endpoint' do - get api('/sidekiq/queue_metrics', admin) + get api('/sidekiq/queue_metrics', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to match a_hash_including( @@ -25,14 +32,14 @@ RSpec.describe API::SidekiqMetrics, feature_category: :not_owned do end it 'defines the `process_metrics` endpoint' do - get api('/sidekiq/process_metrics', admin) + get api('/sidekiq/process_metrics', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['processes']).to be_an Array end it 'defines the `job_stats` endpoint' do - get api('/sidekiq/job_stats', admin) + get api('/sidekiq/job_stats', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_a Hash @@ -43,7 +50,7 @@ RSpec.describe API::SidekiqMetrics, feature_category: :not_owned do end it 'defines the `compound_metrics` endpoint' do - get api('/sidekiq/compound_metrics', admin) + get api('/sidekiq/compound_metrics', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_a Hash diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 2bc4c177bc9..4ba2a768e01 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Snippets, factory_default: :keep, feature_category: :source_code_management do +RSpec.describe API::Snippets, :aggregate_failures, factory_default: :keep, feature_category: :source_code_management do include SnippetHelpers let_it_be(:admin) { create(:user, :admin) } @@ -448,7 +448,7 @@ RSpec.describe API::Snippets, factory_default: :keep, feature_category: :source_ end context "when admin" do - let_it_be(:token) { create(:personal_access_token, user: admin, scopes: [:sudo]) } + let_it_be(:token) { create(:personal_access_token, :admin_mode, user: admin, scopes: [:sudo]) } subject do put api("/snippets/#{snippet.id}", personal_access_token: token), params: { visibility: 'private', sudo: user.id } @@ -499,23 +499,19 @@ RSpec.describe API::Snippets, factory_default: :keep, feature_category: :source_ end describe "GET /snippets/:id/user_agent_detail" do - let(:snippet) { public_snippet } + let(:path) { "/snippets/#{public_snippet.id}/user_agent_detail" } - it 'exposes known attributes' do - user_agent_detail = create(:user_agent_detail, subject: snippet) + let_it_be(:user_agent_detail) { create(:user_agent_detail, subject: public_snippet) } + + it_behaves_like 'GET request permissions for admin mode' - get api("/snippets/#{snippet.id}/user_agent_detail", admin) + it 'exposes known attributes' do + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['user_agent']).to eq(user_agent_detail.user_agent) expect(json_response['ip_address']).to eq(user_agent_detail.ip_address) expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted) end - - it "returns unauthorized for non-admin users" do - get api("/snippets/#{snippet.id}/user_agent_detail", user) - - expect(response).to have_gitlab_http_status(:forbidden) - end end end diff --git a/spec/requests/api/statistics_spec.rb b/spec/requests/api/statistics_spec.rb index 85fed48a077..baac39abf2c 100644 --- a/spec/requests/api/statistics_spec.rb +++ b/spec/requests/api/statistics_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Statistics, 'Statistics', feature_category: :devops_reports do +RSpec.describe API::Statistics, 'Statistics', :aggregate_failures, feature_category: :devops_reports do include ProjectForksHelper tables_to_analyze = %w[ projects @@ -21,6 +21,8 @@ RSpec.describe API::Statistics, 'Statistics', feature_category: :devops_reports let(:path) { "/application/statistics" } describe "GET /application/statistics" do + it_behaves_like 'GET request permissions for admin mode' + context 'when no user' do it "returns authentication error" do get api(path, nil) @@ -43,7 +45,7 @@ RSpec.describe API::Statistics, 'Statistics', feature_category: :devops_reports let(:admin) { create(:admin) } it 'matches the response schema' do - get api(path, admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('statistics') @@ -66,7 +68,7 @@ RSpec.describe API::Statistics, 'Statistics', feature_category: :devops_reports ApplicationRecord.connection.execute("ANALYZE #{table}") end - get api(path, admin) + get api(path, admin, admin_mode: true) expected_statistics = { issues: 2, diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index ab5e04246e8..604631bbf7f 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -178,7 +178,7 @@ RSpec.describe API::Tags, feature_category: :source_code_management do end end - context 'with keyset pagination option', :aggregate_errors do + context 'with keyset pagination option', :aggregate_failures do let(:base_params) { { pagination: 'keyset' } } context 'with gitaly pagination params' do diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb index 2bd7cb027aa..f479ca25f3c 100644 --- a/spec/requests/api/terraform/modules/v1/packages_spec.rb +++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb @@ -415,12 +415,15 @@ RSpec.describe API::Terraform::Modules::V1::Packages, feature_category: :package with_them do let(:snowplow_gitlab_standard_context) do - { + context = { project: project, - user: user_role == :anonymous ? nil : user, namespace: project.namespace, property: 'i_package_terraform_module_user' } + + context[:user] = user if user_role != :anonymous + + context end before do diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb index fd34345d814..4c9f930df2f 100644 --- a/spec/requests/api/terraform/state_spec.rb +++ b/spec/requests/api/terraform/state_spec.rb @@ -21,6 +21,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu before do stub_terraform_state_object_storage + stub_config(terraform_state: { enabled: true }) end shared_examples 'endpoint with unique user tracking' do @@ -51,7 +52,6 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu it_behaves_like 'Snowplow event tracking with RedisHLL context' do subject(:api_request) { request } - let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } let(:category) { described_class.name } let(:action) { 'terraform_state_api_request' } let(:label) { 'redis_hll_counters.terraform.p_terraform_state_api_unique_users_monthly' } @@ -82,6 +82,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu subject(:request) { get api(state_path), headers: auth_header } it_behaves_like 'endpoint with unique user tracking' + it_behaves_like 'it depends on value of the `terraform_state.enabled` config' context 'without authentication' do let(:auth_header) { basic_auth_header('bad', 'token') } @@ -113,17 +114,6 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu end end - context 'allow_dots_on_tf_state_names is disabled, and the state name contains a dot' do - let(:state_name) { 'state-name-with-dot' } - let(:state_path) { "/projects/#{project_id}/terraform/state/#{state_name}.tfstate" } - - before do - stub_feature_flags(allow_dots_on_tf_state_names: false) - end - - it_behaves_like 'can access terraform state' - end - context 'for a project that does not exist' do let(:project_id) { '0000' } @@ -194,6 +184,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu subject(:request) { post api(state_path), headers: auth_header, as: :json, params: params } it_behaves_like 'endpoint with unique user tracking' + it_behaves_like 'it depends on value of the `terraform_state.enabled` config' context 'when terraform state with a given name is already present' do context 'with maintainer permissions' do @@ -275,21 +266,6 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu expect(Gitlab::Json.parse(response.body)).to be_empty end end - - context 'allow_dots_on_tf_state_names is disabled, and the state name contains a dot' do - let(:non_existing_state_name) { 'state-name-with-dot.tfstate' } - - before do - stub_feature_flags(allow_dots_on_tf_state_names: false) - end - - it 'strips characters after the dot' do - expect { request }.to change { Terraform::State.count }.by(1) - - expect(response).to have_gitlab_http_status(:ok) - expect(Terraform::State.last.name).to eq('state-name-with-dot') - end - end end context 'without body' do @@ -372,6 +348,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu subject(:request) { delete api(state_path), headers: auth_header } it_behaves_like 'endpoint with unique user tracking' + it_behaves_like 'it depends on value of the `terraform_state.enabled` config' shared_examples 'schedules the state for deletion' do it 'returns empty body' do @@ -396,18 +373,6 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu it_behaves_like 'schedules the state for deletion' end - context 'allow_dots_on_tf_state_names is disabled, and the state name contains a dot' do - let(:state_name) { 'state-name-with-dot' } - let(:state_name_with_dot) { "#{state_name}.tfstate" } - let(:state_path) { "/projects/#{project_id}/terraform/state/#{state_name_with_dot}" } - - before do - stub_feature_flags(allow_dots_on_tf_state_names: false) - end - - it_behaves_like 'schedules the state for deletion' - end - context 'with invalid state name' do let(:state_name) { 'foo/bar' } @@ -469,6 +434,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu request expect(response).to have_gitlab_http_status(:conflict) + expect(Gitlab::Json.parse(response.body)).to include('Who' => current_user.username) end end @@ -496,30 +462,10 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu context 'with a dot in the state name' do let(:state_name) { 'test.state' } - context 'with allow_dots_on_tf_state_names ff enabled' do - before do - stub_feature_flags(allow_dots_on_tf_state_names: true) - end - - let(:state_name) { 'test.state' } - - it 'locks the terraform state' do - request - - expect(response).to have_gitlab_http_status(:ok) - end - end - - context 'with allow_dots_on_tf_state_names ff disabled' do - before do - stub_feature_flags(allow_dots_on_tf_state_names: false) - end - - it 'returns 404' do - request + it 'locks the terraform state' do + request - expect(response).to have_gitlab_http_status(:not_found) - end + expect(response).to have_gitlab_http_status(:ok) end end end @@ -540,7 +486,6 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu before do state.lock_xid = '123.456' state.save! - stub_feature_flags(allow_dots_on_tf_state_names: true) end subject(:request) { delete api("#{state_path}/lock"), headers: auth_header, params: params } @@ -553,6 +498,10 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu let(:lock_id) { 'irrelevant to this test, just needs to be present' } end + it_behaves_like 'it depends on value of the `terraform_state.enabled` config' do + let(:lock_id) { '123.456' } + end + where(given_state_name: %w[test-state test.state test%2Ffoo]) with_them do let(:state_name) { given_state_name } @@ -567,23 +516,6 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu end end - context 'with allow_dots_on_tf_state_names ff disabled' do - before do - stub_feature_flags(allow_dots_on_tf_state_names: false) - end - - context 'with dots in the state name' do - let(:lock_id) { '123.456' } - let(:state_name) { 'test.state' } - - it 'returns 404' do - request - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - context 'with no lock id (force-unlock)' do let(:params) { {} } diff --git a/spec/requests/api/terraform/state_version_spec.rb b/spec/requests/api/terraform/state_version_spec.rb index 28abbb5749d..94fd2984435 100644 --- a/spec/requests/api/terraform/state_version_spec.rb +++ b/spec/requests/api/terraform/state_version_spec.rb @@ -10,7 +10,7 @@ RSpec.describe API::Terraform::StateVersion, feature_category: :infrastructure_a let_it_be(:maintainer) { create(:user, maintainer_projects: [project]) } let_it_be(:user_without_access) { create(:user) } - let_it_be(:state) { create(:terraform_state, project: project) } + let_it_be_with_reload(:state) { create(:terraform_state, project: project) } let!(:versions) { create_list(:terraform_state_version, 3, terraform_state: state) } @@ -22,9 +22,15 @@ RSpec.describe API::Terraform::StateVersion, feature_category: :infrastructure_a let(:version_serial) { version.version } let(:state_version_path) { "/projects/#{project_id}/terraform/state/#{state_name}/versions/#{version_serial}" } + before do + stub_config(terraform_state: { enabled: true }) + end + describe 'GET /projects/:id/terraform/state/:name/versions/:serial' do subject(:request) { get api(state_version_path), headers: auth_header } + it_behaves_like 'it depends on value of the `terraform_state.enabled` config' + context 'with invalid authentication' do let(:auth_header) { basic_auth_header('bad', 'token') } @@ -147,6 +153,8 @@ RSpec.describe API::Terraform::StateVersion, feature_category: :infrastructure_a describe 'DELETE /projects/:id/terraform/state/:name/versions/:serial' do subject(:request) { delete api(state_version_path), headers: auth_header } + it_behaves_like 'it depends on value of the `terraform_state.enabled` config', { success_status: :no_content } + context 'with invalid authentication' do let(:auth_header) { basic_auth_header('bad', 'token') } diff --git a/spec/requests/api/topics_spec.rb b/spec/requests/api/topics_spec.rb index 14719292557..560f22c94be 100644 --- a/spec/requests/api/topics_spec.rb +++ b/spec/requests/api/topics_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Topics, feature_category: :projects do +RSpec.describe API::Topics, :aggregate_failures, feature_category: :projects do include WorkhorseHelpers let_it_be(:file) { fixture_file_upload('spec/fixtures/dk.png') } @@ -14,9 +14,11 @@ RSpec.describe API::Topics, feature_category: :projects do let_it_be(:admin) { create(:user, :admin) } let_it_be(:user) { create(:user) } - describe 'GET /topics', :aggregate_failures do + let(:path) { '/topics' } + + describe 'GET /topics' do it 'returns topics ordered by total_projects_count' do - get api('/topics') + get api(path) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -40,13 +42,13 @@ RSpec.describe API::Topics, feature_category: :projects do let_it_be(:topic_4) { create(:topic, name: 'unassigned topic', total_projects_count: 0) } it 'returns topics without assigned projects' do - get api('/topics'), params: { without_projects: true } + get api(path), params: { without_projects: true } expect(json_response.map { |t| t['id'] }).to contain_exactly(topic_4.id) end it 'returns topics without assigned projects' do - get api('/topics'), params: { without_projects: false } + get api(path), params: { without_projects: false } expect(json_response.map { |t| t['id'] }).to contain_exactly(topic_1.id, topic_2.id, topic_3.id, topic_4.id) end @@ -66,7 +68,7 @@ RSpec.describe API::Topics, feature_category: :projects do with_them do it 'returns filtered topics' do - get api('/topics'), params: { search: search } + get api(path), params: { search: search } expect(json_response.map { |t| t['name'] }).to eq(result) end @@ -97,7 +99,7 @@ RSpec.describe API::Topics, feature_category: :projects do with_them do it 'returns paginated topics' do - get api('/topics'), params: params + get api(path), params: params expect(json_response.map { |t| t['name'] }).to eq(result) end @@ -105,7 +107,7 @@ RSpec.describe API::Topics, feature_category: :projects do end end - describe 'GET /topic/:id', :aggregate_failures do + describe 'GET /topic/:id' do it 'returns topic' do get api("/topics/#{topic_2.id}") @@ -130,10 +132,14 @@ RSpec.describe API::Topics, feature_category: :projects do end end - describe 'POST /topics', :aggregate_failures do + describe 'POST /topics' do + let(:params) { { name: 'my-topic', title: 'My Topic' } } + + it_behaves_like 'POST request permissions for admin mode' + context 'as administrator' do it 'creates a topic' do - post api('/topics/', admin), params: { name: 'my-topic', title: 'My Topic' } + post api('/topics/', admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:created) expect(json_response['name']).to eq('my-topic') @@ -142,7 +148,7 @@ RSpec.describe API::Topics, feature_category: :projects do it 'creates a topic with avatar and description' do workhorse_form_with_file( - api('/topics/', admin), + api('/topics/', admin, admin_mode: true), file_key: :avatar, params: { name: 'my-topic', title: 'My Topic', description: 'my description...', avatar: file } ) @@ -160,14 +166,14 @@ RSpec.describe API::Topics, feature_category: :projects do end it 'returns 400 if name is not unique (case insensitive)' do - post api('/topics/', admin), params: { name: topic_1.name.downcase, title: 'My Topic' } + post api('/topics/', admin, admin_mode: true), params: { name: topic_1.name.downcase, title: 'My Topic' } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']['name']).to eq(['has already been taken']) end it 'returns 400 if title is missing' do - post api('/topics/', admin), params: { name: 'my-topic' } + post api('/topics/', admin, admin_mode: true), params: { name: 'my-topic' } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eql('title is missing') @@ -176,7 +182,7 @@ RSpec.describe API::Topics, feature_category: :projects do context 'as normal user' do it 'returns 403 Forbidden' do - post api('/topics/', user), params: { name: 'my-topic', title: 'My Topic' } + post api('/topics/', user), params: params expect(response).to have_gitlab_http_status(:forbidden) end @@ -184,17 +190,23 @@ RSpec.describe API::Topics, feature_category: :projects do context 'as anonymous' do it 'returns 401 Unauthorized' do - post api('/topics/'), params: { name: 'my-topic', title: 'My Topic' } + post api('/topics/'), params: params expect(response).to have_gitlab_http_status(:unauthorized) end end end - describe 'PUT /topics', :aggregate_failures do + describe 'PUT /topics' do + let(:params) { { name: 'my-topic' } } + + it_behaves_like 'PUT request permissions for admin mode' do + let(:path) { "/topics/#{topic_3.id}" } + end + context 'as administrator' do it 'updates a topic' do - put api("/topics/#{topic_3.id}", admin), params: { name: 'my-topic' } + put api("/topics/#{topic_3.id}", admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:ok) expect(json_response['name']).to eq('my-topic') @@ -203,7 +215,7 @@ RSpec.describe API::Topics, feature_category: :projects do it 'updates a topic with avatar and description' do workhorse_form_with_file( - api("/topics/#{topic_3.id}", admin), + api("/topics/#{topic_3.id}", admin, admin_mode: true), method: :put, file_key: :avatar, params: { description: 'my description...', avatar: file } @@ -215,7 +227,7 @@ RSpec.describe API::Topics, feature_category: :projects do end it 'keeps avatar when updating other fields' do - put api("/topics/#{topic_1.id}", admin), params: { name: 'my-topic' } + put api("/topics/#{topic_1.id}", admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:ok) expect(json_response['name']).to eq('my-topic') @@ -223,13 +235,13 @@ RSpec.describe API::Topics, feature_category: :projects do end it 'returns 404 for non existing id' do - put api("/topics/#{non_existing_record_id}", admin), params: { name: 'my-topic' } + put api("/topics/#{non_existing_record_id}", admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:not_found) end it 'returns 400 for invalid `id` parameter' do - put api('/topics/invalid', admin), params: { name: 'my-topic' } + put api('/topics/invalid', admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eql('id is invalid') @@ -237,7 +249,7 @@ RSpec.describe API::Topics, feature_category: :projects do context 'with blank avatar' do it 'removes avatar' do - put api("/topics/#{topic_1.id}", admin), params: { avatar: '' } + put api("/topics/#{topic_1.id}", admin, admin_mode: true), params: { avatar: '' } expect(response).to have_gitlab_http_status(:ok) expect(json_response['avatar_url']).to be_nil @@ -245,7 +257,7 @@ RSpec.describe API::Topics, feature_category: :projects do end it 'removes avatar besides other changes' do - put api("/topics/#{topic_1.id}", admin), params: { name: 'new-topic-name', avatar: '' } + put api("/topics/#{topic_1.id}", admin, admin_mode: true), params: { name: 'new-topic-name', avatar: '' } expect(response).to have_gitlab_http_status(:ok) expect(json_response['name']).to eq('new-topic-name') @@ -254,7 +266,7 @@ RSpec.describe API::Topics, feature_category: :projects do end it 'does not remove avatar in case of other errors' do - put api("/topics/#{topic_1.id}", admin), params: { name: topic_2.name, avatar: '' } + put api("/topics/#{topic_1.id}", admin, admin_mode: true), params: { name: topic_2.name, avatar: '' } expect(response).to have_gitlab_http_status(:bad_request) expect(topic_1.reload.avatar_url).not_to be_nil @@ -264,7 +276,7 @@ RSpec.describe API::Topics, feature_category: :projects do context 'as normal user' do it 'returns 403 Forbidden' do - put api("/topics/#{topic_3.id}", user), params: { name: 'my-topic' } + put api("/topics/#{topic_3.id}", user), params: params expect(response).to have_gitlab_http_status(:forbidden) end @@ -272,29 +284,37 @@ RSpec.describe API::Topics, feature_category: :projects do context 'as anonymous' do it 'returns 401 Unauthorized' do - put api("/topics/#{topic_3.id}"), params: { name: 'my-topic' } + put api("/topics/#{topic_3.id}"), params: params expect(response).to have_gitlab_http_status(:unauthorized) end end end - describe 'DELETE /topics', :aggregate_failures do + describe 'DELETE /topics/:id' do + let(:params) { { name: 'my-topic' } } + context 'as administrator' do - it 'deletes a topic' do - delete api("/topics/#{topic_3.id}", admin), params: { name: 'my-topic' } + it 'deletes a topic with admin mode' do + delete api("/topics/#{topic_3.id}", admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:no_content) end + it 'deletes a topic without admin mode' do + delete api("/topics/#{topic_3.id}", admin, admin_mode: false), params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + it 'returns 404 for non existing id' do - delete api("/topics/#{non_existing_record_id}", admin), params: { name: 'my-topic' } + delete api("/topics/#{non_existing_record_id}", admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:not_found) end it 'returns 400 for invalid `id` parameter' do - delete api('/topics/invalid', admin), params: { name: 'my-topic' } + delete api('/topics/invalid', admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eql('id is invalid') @@ -303,7 +323,7 @@ RSpec.describe API::Topics, feature_category: :projects do context 'as normal user' do it 'returns 403 Forbidden' do - delete api("/topics/#{topic_3.id}", user), params: { name: 'my-topic' } + delete api("/topics/#{topic_3.id}", user), params: params expect(response).to have_gitlab_http_status(:forbidden) end @@ -311,16 +331,21 @@ RSpec.describe API::Topics, feature_category: :projects do context 'as anonymous' do it 'returns 401 Unauthorized' do - delete api("/topics/#{topic_3.id}"), params: { name: 'my-topic' } + delete api("/topics/#{topic_3.id}"), params: params expect(response).to have_gitlab_http_status(:unauthorized) end end end - describe 'POST /topics/merge', :aggregate_failures do + describe 'POST /topics/merge' do + it_behaves_like 'POST request permissions for admin mode' do + let(:path) { '/topics/merge' } + let(:params) { { source_topic_id: topic_3.id, target_topic_id: topic_2.id } } + end + context 'as administrator' do - let_it_be(:api_url) { api('/topics/merge', admin) } + let_it_be(:api_url) { api('/topics/merge', admin, admin_mode: true) } it 'merge topics' do post api_url, params: { source_topic_id: topic_3.id, target_topic_id: topic_2.id } diff --git a/spec/requests/api/unleash_spec.rb b/spec/requests/api/unleash_spec.rb index 5daf7cd7b75..75b26b98228 100644 --- a/spec/requests/api/unleash_spec.rb +++ b/spec/requests/api/unleash_spec.rb @@ -88,6 +88,14 @@ RSpec.describe API::Unleash, feature_category: :feature_flags do end end + describe 'GET /feature_flags/unleash/:project_id/client/features', :use_clean_rails_redis_caching do + specify do + get api("/feature_flags/unleash/#{project_id}/client/features"), params: params, headers: headers + + is_expected.to have_request_urgency(:medium) + end + end + %w(/feature_flags/unleash/:project_id/features /feature_flags/unleash/:project_id/client/features).each do |features_endpoint| describe "GET #{features_endpoint}", :use_clean_rails_redis_caching do let(:features_url) { features_endpoint.sub(':project_id', project_id.to_s) } diff --git a/spec/requests/api/usage_data_non_sql_metrics_spec.rb b/spec/requests/api/usage_data_non_sql_metrics_spec.rb index 0a6f248af2c..b2929caf676 100644 --- a/spec/requests/api/usage_data_non_sql_metrics_spec.rb +++ b/spec/requests/api/usage_data_non_sql_metrics_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::UsageDataNonSqlMetrics, feature_category: :service_ping do +RSpec.describe API::UsageDataNonSqlMetrics, :aggregate_failures, feature_category: :service_ping do include UsageDataHelpers let_it_be(:admin) { create(:user, admin: true) } @@ -21,8 +21,12 @@ RSpec.describe API::UsageDataNonSqlMetrics, feature_category: :service_ping do stub_database_flavor_check end + it_behaves_like 'GET request permissions for admin mode' do + let(:path) { endpoint } + end + it 'returns non sql metrics if user is admin' do - get api(endpoint, admin) + get api(endpoint, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['counts']).to be_a(Hash) @@ -53,7 +57,7 @@ RSpec.describe API::UsageDataNonSqlMetrics, feature_category: :service_ping do end it 'returns not_found for admin' do - get api(endpoint, admin) + get api(endpoint, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end diff --git a/spec/requests/api/usage_data_queries_spec.rb b/spec/requests/api/usage_data_queries_spec.rb index e556064025c..ab3c38adb81 100644 --- a/spec/requests/api/usage_data_queries_spec.rb +++ b/spec/requests/api/usage_data_queries_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' require 'rake_helper' -RSpec.describe API::UsageDataQueries, feature_category: :service_ping do +RSpec.describe API::UsageDataQueries, :aggregate_failures, feature_category: :service_ping do include UsageDataHelpers let_it_be(:admin) { create(:user, admin: true) } @@ -22,8 +22,12 @@ RSpec.describe API::UsageDataQueries, feature_category: :service_ping do stub_feature_flags(usage_data_queries_api: true) end + it_behaves_like 'GET request permissions for admin mode' do + let(:path) { endpoint } + end + it 'returns queries if user is admin' do - get api(endpoint, admin) + get api(endpoint, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['active_user_count']).to start_with('SELECT COUNT("users"."id") FROM "users"') @@ -54,7 +58,7 @@ RSpec.describe API::UsageDataQueries, feature_category: :service_ping do end it 'returns not_found for admin' do - get api(endpoint, admin) + get api(endpoint, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end @@ -81,7 +85,7 @@ RSpec.describe API::UsageDataQueries, feature_category: :service_ping do it 'matches the generated query' do travel_to(Time.utc(2021, 1, 1)) do - get api(endpoint, admin) + get api(endpoint, admin, admin_mode: true) end data = Gitlab::Json.parse(File.read(file)) diff --git a/spec/requests/api/users_preferences_spec.rb b/spec/requests/api/users_preferences_spec.rb index ef9735fd8b0..067acd150f3 100644 --- a/spec/requests/api/users_preferences_spec.rb +++ b/spec/requests/api/users_preferences_spec.rb @@ -10,17 +10,20 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns a success status and the value has been changed' do put api("/user/preferences", user), params: { view_diffs_file_by_file: true, - show_whitespace_in_diffs: true + show_whitespace_in_diffs: true, + pass_user_identities_to_ci_jwt: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response['view_diffs_file_by_file']).to eq(true) expect(json_response['show_whitespace_in_diffs']).to eq(true) + expect(json_response['pass_user_identities_to_ci_jwt']).to eq(true) user.reload expect(user.view_diffs_file_by_file).to be_truthy expect(user.show_whitespace_in_diffs).to be_truthy + expect(user.pass_user_identities_to_ci_jwt).to be_truthy end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 34867b13db2..cc8be312c71 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Users, feature_category: :user_profile do +RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile do include WorkhorseHelpers let_it_be(:admin) { create(:admin) } @@ -27,9 +27,15 @@ RSpec.describe API::Users, feature_category: :user_profile do let_it_be(:user, reload: true) { create(:user, note: '2018-11-05 | 2FA removed | user requested | www.gitlab.com') } describe 'POST /users' do + let(:path) { '/users' } + + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { attributes_for(:user).merge({ note: 'Awesome Note' }) } + end + context 'when unauthenticated' do it 'return authentication error' do - post api('/users') + post api(path) expect(response).to have_gitlab_http_status(:unauthorized) end @@ -41,7 +47,7 @@ RSpec.describe API::Users, feature_category: :user_profile do optional_attributes = { note: 'Awesome Note' } attributes = attributes_for(:user).merge(optional_attributes) - post api('/users', admin), params: attributes + post api(path, admin, admin_mode: true), params: attributes expect(response).to have_gitlab_http_status(:created) expect(json_response['note']).to eq(optional_attributes[:note]) @@ -50,7 +56,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'as a regular user' do it 'does not allow creating new user' do - post api('/users', user), params: attributes_for(:user) + post api(path, user), params: attributes_for(:user) expect(response).to have_gitlab_http_status(:forbidden) end @@ -59,12 +65,18 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe "PUT /users/:id" do + let(:path) { "/users/#{user.id}" } + + it_behaves_like 'PUT request permissions for admin mode' do + let(:params) { { note: 'new note' } } + end + context 'when user is an admin' do it "updates note of the user" do new_note = '2019-07-07 | Email changed | user requested | www.gitlab.com' expect do - put api("/users/#{user.id}", admin), params: { note: new_note } + put api(path, admin, admin_mode: true), params: { note: new_note } end.to change { user.reload.note } .from('2018-11-05 | 2FA removed | user requested | www.gitlab.com') .to(new_note) @@ -77,7 +89,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'when user is not an admin' do it "cannot update their own note" do expect do - put api("/users/#{user.id}", user), params: { note: 'new note' } + put api(path, user), params: { note: 'new note' } end.not_to change { user.reload.note } expect(response).to have_gitlab_http_status(:forbidden) @@ -89,7 +101,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context "when current user is an admin" do it "returns a 204 when 2FA is disabled for the target user" do expect do - patch api("/users/#{user_with_2fa.id}/disable_two_factor", admin) + patch api("/users/#{user_with_2fa.id}/disable_two_factor", admin, admin_mode: true) end.to change { user_with_2fa.reload.two_factor_enabled? } .from(true) .to(false) @@ -103,14 +115,14 @@ RSpec.describe API::Users, feature_category: :user_profile do .and_return(destroy_service) expect(destroy_service).to receive(:execute) - patch api("/users/#{user_with_2fa.id}/disable_two_factor", admin) + patch api("/users/#{user_with_2fa.id}/disable_two_factor", admin, admin_mode: true) end it "returns a 400 if 2FA is not enabled for the target user" do expect(TwoFactor::DestroyService).to receive(:new).and_call_original expect do - patch api("/users/#{user.id}/disable_two_factor", admin) + patch api("/users/#{user.id}/disable_two_factor", admin, admin_mode: true) end.not_to change { user.reload.two_factor_enabled? } expect(response).to have_gitlab_http_status(:bad_request) @@ -121,7 +133,7 @@ RSpec.describe API::Users, feature_category: :user_profile do expect(TwoFactor::DestroyService).not_to receive(:new) expect do - patch api("/users/#{admin_with_2fa.id}/disable_two_factor", admin) + patch api("/users/#{admin_with_2fa.id}/disable_two_factor", admin, admin_mode: true) end.not_to change { admin_with_2fa.reload.two_factor_enabled? } expect(response).to have_gitlab_http_status(:forbidden) @@ -131,7 +143,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it "returns a 404 if the target user cannot be found" do expect(TwoFactor::DestroyService).not_to receive(:new) - patch api("/users/#{non_existing_record_id}/disable_two_factor", admin) + patch api("/users/#{non_existing_record_id}/disable_two_factor", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq("404 User Not Found") @@ -159,9 +171,11 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe 'GET /users/' do + let(:path) { '/users' } + context 'when unauthenticated' do it "does not contain certain fields" do - get api("/users"), params: { username: user.username } + get api(path), params: { username: user.username } expect(json_response.first).not_to have_key('note') expect(json_response.first).not_to have_key('namespace_id') @@ -172,8 +186,9 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'when authenticated' do context 'as a regular user' do it 'does not contain certain fields' do - get api("/users", user), params: { username: user.username } + get api(path, user), params: { username: user.username } + expect(response).to have_gitlab_http_status(:ok) expect(json_response.first).not_to have_key('note') expect(json_response.first).not_to have_key('namespace_id') expect(json_response.first).not_to have_key('created_by') @@ -182,7 +197,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'as an admin' do it 'contains the note of users' do - get api("/users", admin), params: { username: user.username } + get api(path, admin, admin_mode: true), params: { username: user.username } expect(response).to have_gitlab_http_status(:success) expect(json_response.first).to have_key('note') @@ -191,7 +206,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'with `created_by` details' do it 'has created_by as nil with a self-registered account' do - get api("/users", admin), params: { username: user.username } + get api(path, admin, admin_mode: true), params: { username: user.username } expect(response).to have_gitlab_http_status(:success) expect(json_response.first).to have_key('created_by') @@ -201,7 +216,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'is created_by a user and has those details' do created = create(:user, created_by_id: user.id) - get api("/users", admin), params: { username: created.username } + get api(path, admin, admin_mode: true), params: { username: created.username } expect(response).to have_gitlab_http_status(:success) expect(json_response.first['created_by'].symbolize_keys) @@ -217,7 +232,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'avoids N+1 queries when requested by admin' do control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do - get api("/users", admin) + get api(path, admin) end.count create_list(:user, 3) @@ -227,19 +242,19 @@ RSpec.describe API::Users, feature_category: :user_profile do # Refer issue https://gitlab.com/gitlab-org/gitlab/-/issues/367080 expect do - get api("/users", admin) + get api(path, admin) end.not_to exceed_all_query_limit(control_count + 3) end it 'avoids N+1 queries when requested by a regular user' do control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do - get api("/users", user) + get api(path, user) end.count create_list(:user, 3) expect do - get api("/users", user) + get api(path, user) end.not_to exceed_all_query_limit(control_count) end end @@ -247,11 +262,13 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe 'GET /user' do + let(:path) { '/user' } + context 'when authenticated' do context 'as an admin' do context 'accesses their own profile' do it 'contains the note of the user' do - get api("/user", admin) + get api(path, admin, admin_mode: true) expect(json_response).to have_key('note') expect(json_response['note']).to eq(admin.note) @@ -259,7 +276,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end context 'sudo' do - let(:admin_personal_access_token) { create(:personal_access_token, user: admin, scopes: %w[api sudo]).token } + let(:admin_personal_access_token) { create(:personal_access_token, :admin_mode, user: admin, scopes: %w[api sudo]).token } context 'accesses the profile of another regular user' do it 'does not contain the note of the user' do @@ -286,7 +303,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'as a regular user' do it 'does not contain the note of the user' do - get api("/user", user) + get api(path, user) expect(json_response).not_to have_key('note') expect(json_response).not_to have_key('namespace_id') @@ -318,15 +335,17 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe 'GET /users' do + let(:path) { '/users' } + context "when unauthenticated" do it "returns authorization error when the `username` parameter is not passed" do - get api("/users") + get api(path) expect(response).to have_gitlab_http_status(:forbidden) end it "returns the user when a valid `username` parameter is passed" do - get api("/users"), params: { username: user.username } + get api(path), params: { username: user.username } expect(response).to match_response_schema('public_api/v4/user/basics') expect(json_response.size).to eq(1) @@ -335,7 +354,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "returns the user when a valid `username` parameter is passed (case insensitive)" do - get api("/users"), params: { username: user.username.upcase } + get api(path), params: { username: user.username.upcase } expect(response).to match_response_schema('public_api/v4/user/basics') expect(json_response.size).to eq(1) @@ -344,7 +363,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "returns an empty response when an invalid `username` parameter is passed" do - get api("/users"), params: { username: 'invalid' } + get api(path), params: { username: 'invalid' } expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array @@ -352,14 +371,14 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "does not return the highest role" do - get api("/users"), params: { username: user.username } + get api(path), params: { username: user.username } expect(response).to match_response_schema('public_api/v4/user/basics') expect(json_response.first.keys).not_to include 'highest_role' end it "does not return the current or last sign-in ip addresses" do - get api("/users"), params: { username: user.username } + get api(path), params: { username: user.username } expect(response).to match_response_schema('public_api/v4/user/basics') expect(json_response.first.keys).not_to include 'current_sign_in_ip' @@ -372,13 +391,13 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "returns authorization error when the `username` parameter refers to an inaccessible user" do - get api("/users"), params: { username: user.username } + get api(path), params: { username: user.username } expect(response).to have_gitlab_http_status(:forbidden) end it "returns authorization error when the `username` parameter is not passed" do - get api("/users") + get api(path) expect(response).to have_gitlab_http_status(:forbidden) end @@ -394,7 +413,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'when authenticate as a regular user' do it "renders 200" do - get api("/users", user) + get api(path, user) expect(response).to match_response_schema('public_api/v4/user/basics') end @@ -402,7 +421,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'when authenticate as an admin' do it "renders 200" do - get api("/users", admin) + get api(path, admin) expect(response).to match_response_schema('public_api/v4/user/basics') end @@ -410,7 +429,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "returns an array of users" do - get api("/users", user) + get api(path, user) expect(response).to match_response_schema('public_api/v4/user/basics') expect(response).to include_pagination_headers @@ -466,7 +485,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'does not reveal the `is_admin` flag of the user' do - get api('/users', user) + get api(path, user) expect(response).to match_response_schema('public_api/v4/user/basics') expect(json_response.first.keys).not_to include 'is_admin' @@ -528,7 +547,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context "when admin" do context 'when sudo is defined' do it 'does not return 500' do - admin_personal_access_token = create(:personal_access_token, user: admin, scopes: [:sudo]) + admin_personal_access_token = create(:personal_access_token, :admin_mode, user: admin, scopes: [:sudo]) get api("/users?sudo=#{user.id}", admin, personal_access_token: admin_personal_access_token) expect(response).to have_gitlab_http_status(:success) @@ -536,14 +555,14 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "returns an array of users" do - get api("/users", admin) + get api(path, admin, admin_mode: true) expect(response).to match_response_schema('public_api/v4/user/admins') expect(response).to include_pagination_headers end it "users contain the `namespace_id` field" do - get api("/users", admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:success) expect(response).to match_response_schema('public_api/v4/user/admins') @@ -554,7 +573,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it "returns an array of external users" do create(:user, external: true) - get api("/users?external=true", admin) + get api("/users?external=true", admin, admin_mode: true) expect(response).to match_response_schema('public_api/v4/user/admins') expect(response).to include_pagination_headers @@ -562,7 +581,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "returns one user by external UID" do - get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}&provider=#{omniauth_user.identities.first.provider}", admin) + get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}&provider=#{omniauth_user.identities.first.provider}", admin, admin_mode: true) expect(response).to match_response_schema('public_api/v4/user/admins') expect(json_response.size).to eq(1) @@ -570,13 +589,13 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "returns 400 error if provider with no extern_uid" do - get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}", admin) + get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) end it "returns 400 error if provider with no extern_uid" do - get api("/users?provider=#{omniauth_user.identities.first.provider}", admin) + get api("/users?provider=#{omniauth_user.identities.first.provider}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) end @@ -584,7 +603,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it "returns a user created before a specific date" do user = create(:user, created_at: Date.new(2000, 1, 1)) - get api("/users?created_before=2000-01-02T00:00:00.060Z", admin) + get api("/users?created_before=2000-01-02T00:00:00.060Z", admin, admin_mode: true) expect(response).to match_response_schema('public_api/v4/user/admins') expect(json_response.size).to eq(1) @@ -594,7 +613,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it "returns no users created before a specific date" do create(:user, created_at: Date.new(2001, 1, 1)) - get api("/users?created_before=2000-01-02T00:00:00.060Z", admin) + get api("/users?created_before=2000-01-02T00:00:00.060Z", admin, admin_mode: true) expect(response).to match_response_schema('public_api/v4/user/admins') expect(json_response.size).to eq(0) @@ -603,7 +622,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it "returns users created before and after a specific date" do user = create(:user, created_at: Date.new(2001, 1, 1)) - get api("/users?created_before=2001-01-02T00:00:00.060Z&created_after=1999-01-02T00:00:00.060", admin) + get api("/users?created_before=2001-01-02T00:00:00.060Z&created_after=1999-01-02T00:00:00.060", admin, admin_mode: true) expect(response).to match_response_schema('public_api/v4/user/admins') expect(json_response.size).to eq(1) @@ -615,7 +634,7 @@ RSpec.describe API::Users, feature_category: :user_profile do # - admin # - user - get api('/users', admin), params: { order_by: 'id', sort: 'asc' } + get api(path, admin, admin_mode: true), params: { order_by: 'id', sort: 'asc' } expect(response).to match_response_schema('public_api/v4/user/admins') expect(json_response.size).to eq(2) @@ -626,7 +645,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns users with 2fa enabled' do user_with_2fa = create(:user, :two_factor_via_otp) - get api('/users', admin), params: { two_factor: 'enabled' } + get api(path, admin, admin_mode: true), params: { two_factor: 'enabled' } expect(response).to match_response_schema('public_api/v4/user/admins') expect(json_response.size).to eq(1) @@ -638,7 +657,7 @@ RSpec.describe API::Users, feature_category: :user_profile do create(:project, namespace: user.namespace) create(:project, namespace: admin.namespace) - get api('/users', admin), params: { without_projects: true } + get api(path, admin, admin_mode: true), params: { without_projects: true } expect(response).to match_response_schema('public_api/v4/user/admins') expect(json_response.size).to eq(1) @@ -646,7 +665,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'returns 400 when provided incorrect sort params' do - get api('/users', admin), params: { order_by: 'magic', sort: 'asc' } + get api(path, admin, admin_mode: true), params: { order_by: 'magic', sort: 'asc' } expect(response).to have_gitlab_http_status(:bad_request) end @@ -654,7 +673,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'admins param' do it 'returns only admins' do - get api("/users?admins=true", admin) + get api("/users?admins=true", admin, admin_mode: true) expect(response).to match_response_schema('public_api/v4/user/basics') expect(json_response.size).to eq(1) @@ -666,34 +685,36 @@ RSpec.describe API::Users, feature_category: :user_profile do describe "GET /users/:id" do let_it_be(:user2, reload: true) { create(:user, username: 'another_user') } + let(:path) { "/users/#{user.id}" } + before do allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?) .with(:users_get_by_id, scope: user, users_allowlist: []).and_return(false) end it "returns a user by id" do - get api("/users/#{user.id}", user) + get api(path, user) expect(response).to match_response_schema('public_api/v4/user/basic') expect(json_response['username']).to eq(user.username) end it "does not return the user's `is_admin` flag" do - get api("/users/#{user.id}", user) + get api(path, user) expect(response).to match_response_schema('public_api/v4/user/basic') expect(json_response.keys).not_to include 'is_admin' end it "does not return the user's `highest_role`" do - get api("/users/#{user.id}", user) + get api(path, user) expect(response).to match_response_schema('public_api/v4/user/basic') expect(json_response.keys).not_to include 'highest_role' end it "does not return the user's sign in IPs" do - get api("/users/#{user.id}", user) + get api(path, user) expect(response).to match_response_schema('public_api/v4/user/basic') expect(json_response.keys).not_to include 'current_sign_in_ip' @@ -701,7 +722,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "does not contain plan or trial data" do - get api("/users/#{user.id}", user) + get api(path, user) expect(response).to match_response_schema('public_api/v4/user/basic') expect(json_response.keys).not_to include 'plan' @@ -760,7 +781,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'does not contain the note of the user' do - get api("/users/#{user.id}", user) + get api(path, user) expect(json_response).not_to have_key('note') expect(json_response).not_to have_key('sign_in_count') @@ -772,7 +793,7 @@ RSpec.describe API::Users, feature_category: :user_profile do .to receive(:throttled?).with(:users_get_by_id, scope: user, users_allowlist: []) .and_return(false) - get api("/users/#{user.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) end @@ -785,7 +806,7 @@ RSpec.describe API::Users, feature_category: :user_profile do .to receive(:throttled?).with(:users_get_by_id, scope: user, users_allowlist: []) .and_return(true) - get api("/users/#{user.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:too_many_requests) end @@ -794,7 +815,7 @@ RSpec.describe API::Users, feature_category: :user_profile do expect(Gitlab::ApplicationRateLimiter) .not_to receive(:throttled?) - get api("/users/#{user.id}", admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) end @@ -812,7 +833,7 @@ RSpec.describe API::Users, feature_category: :user_profile do .to receive(:throttled?).with(:users_get_by_id, scope: user, users_allowlist: allowlist) .and_call_original - get api("/users/#{user.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) end @@ -827,7 +848,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'returns job title of a user' do - get api("/users/#{user.id}", user) + get api(path, user) expect(response).to match_response_schema('public_api/v4/user/basic') expect(json_response['job_title']).to eq(job_title) @@ -836,7 +857,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'when authenticated as admin' do it 'contains the note of the user' do - get api("/users/#{user.id}", admin) + get api(path, admin, admin_mode: true) expect(json_response).to have_key('note') expect(json_response['note']).to eq(user.note) @@ -844,28 +865,28 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'includes the `is_admin` field' do - get api("/users/#{user.id}", admin) + get api(path, admin, admin_mode: true) expect(response).to match_response_schema('public_api/v4/user/admin') expect(json_response['is_admin']).to be(false) end it "includes the `created_at` field for private users" do - get api("/users/#{private_user.id}", admin) + get api("/users/#{private_user.id}", admin, admin_mode: true) expect(response).to match_response_schema('public_api/v4/user/admin') expect(json_response.keys).to include 'created_at' end it 'includes the `highest_role` field' do - get api("/users/#{user.id}", admin) + get api(path, admin, admin_mode: true) expect(response).to match_response_schema('public_api/v4/user/admin') expect(json_response['highest_role']).to be(0) end it 'includes the `namespace_id` field' do - get api("/users/#{user.id}", admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:success) expect(response).to match_response_schema('public_api/v4/user/admin') @@ -874,13 +895,13 @@ RSpec.describe API::Users, feature_category: :user_profile do if Gitlab.ee? it 'does not include values for plan or trial' do - get api("/users/#{user.id}", admin) + get api(path, admin, admin_mode: true) expect(response).to match_response_schema('public_api/v4/user/basic') end else it 'does not include plan or trial data' do - get api("/users/#{user.id}", admin) + get api(path, admin, admin_mode: true) expect(response).to match_response_schema('public_api/v4/user/basic') expect(json_response.keys).not_to include 'plan' @@ -890,7 +911,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'when user has not logged in' do it 'does not include the sign in IPs' do - get api("/users/#{user.id}", admin) + get api(path, admin, admin_mode: true) expect(response).to match_response_schema('public_api/v4/user/admin') expect(json_response).to include('current_sign_in_ip' => nil, 'last_sign_in_ip' => nil) @@ -901,7 +922,7 @@ RSpec.describe API::Users, feature_category: :user_profile do let_it_be(:signed_in_user) { create(:user, :with_sign_ins) } it 'includes the sign in IPs' do - get api("/users/#{signed_in_user.id}", admin) + get api("/users/#{signed_in_user.id}", admin, admin_mode: true) expect(response).to match_response_schema('public_api/v4/user/admin') expect(json_response['current_sign_in_ip']).to eq('127.0.0.1') @@ -912,14 +933,14 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'for an anonymous user' do it 'returns 403' do - get api("/users/#{user.id}") + get api(path) expect(response).to have_gitlab_http_status(:forbidden) end end it "returns a 404 error if user id not found" do - get api("/users/0", user) + get api("/users/#{non_existing_record_id}", user) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') @@ -954,10 +975,11 @@ RSpec.describe API::Users, feature_category: :user_profile do describe 'POST /users/:id/follow' do let(:followee) { create(:user) } + let(:path) { "/users/#{followee.id}/follow" } context 'on an unfollowed user' do it 'follows the user' do - post api("/users/#{followee.id}/follow", user) + post api(path, user) expect(user.followees).to contain_exactly(followee) expect(response).to have_gitlab_http_status(:created) @@ -967,7 +989,7 @@ RSpec.describe API::Users, feature_category: :user_profile do stub_const('Users::UserFollowUser::MAX_FOLLOWEE_LIMIT', 2) Users::UserFollowUser::MAX_FOLLOWEE_LIMIT.times { user.follow(create(:user)) } - post api("/users/#{followee.id}/follow", user) + post api(path, user) expect(response).to have_gitlab_http_status(:bad_request) expected_message = format(_("You can't follow more than %{limit} users. To follow more users, unfollow some others."), limit: Users::UserFollowUser::MAX_FOLLOWEE_LIMIT) expect(json_response['message']).to eq(expected_message) @@ -981,16 +1003,31 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'does not change following' do - post api("/users/#{followee.id}/follow", user) + post api(path, user) expect(user.followees).to contain_exactly(followee) expect(response).to have_gitlab_http_status(:not_modified) end end + + context 'on a user with disabled following' do + before do + user.enabled_following = false + user.save! + end + + it 'does not change following' do + post api("/users/#{followee.id}/follow", user) + + expect(user.followees).to be_empty + expect(response).to have_gitlab_http_status(:not_modified) + end + end end describe 'POST /users/:id/unfollow' do let(:followee) { create(:user) } + let(:path) { "/users/#{followee.id}/unfollow" } context 'on a followed user' do before do @@ -998,7 +1035,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'unfollow the user' do - post api("/users/#{followee.id}/unfollow", user) + post api(path, user) expect(user.followees).to be_empty expect(response).to have_gitlab_http_status(:created) @@ -1007,7 +1044,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'on an unfollowed user' do it 'does not change following' do - post api("/users/#{followee.id}/unfollow", user) + post api(path, user) expect(user.followees).to be_empty expect(response).to have_gitlab_http_status(:not_modified) @@ -1017,6 +1054,7 @@ RSpec.describe API::Users, feature_category: :user_profile do describe 'GET /users/:id/followers' do let(:follower) { create(:user) } + let(:path) { "/users/#{user.id}/followers" } context 'for an anonymous user' do it 'returns 403' do @@ -1030,7 +1068,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'lists followers' do follower.follow(user) - get api("/users/#{user.id}/followers", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -1049,7 +1087,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'user does not have any follower' do it 'does list nothing' do - get api("/users/#{user.id}/followers", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -1060,6 +1098,7 @@ RSpec.describe API::Users, feature_category: :user_profile do describe 'GET /users/:id/following' do let(:followee) { create(:user) } + let(:path) { "/users/#{user.id}/followers" } context 'for an anonymous user' do it 'returns 403' do @@ -1073,7 +1112,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'lists following user' do user.follow(followee) - get api("/users/#{user.id}/following", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -1092,7 +1131,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'user does not have any follower' do it 'does list nothing' do - get api("/users/#{user.id}/following", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -1102,14 +1141,20 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe "POST /users" do + let(:path) { '/users' } + + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { attributes_for(:user, projects_limit: 3) } + end + it "creates user" do expect do - post api("/users", admin), params: attributes_for(:user, projects_limit: 3) + post api(path, admin, admin_mode: true), params: attributes_for(:user, projects_limit: 3) end.to change { User.count }.by(1) end it "creates user with correct attributes" do - post api('/users', admin), params: attributes_for(:user, admin: true, can_create_group: true) + post api(path, admin, admin_mode: true), params: attributes_for(:user, admin: true, can_create_group: true) expect(response).to have_gitlab_http_status(:created) user_id = json_response['id'] new_user = User.find(user_id) @@ -1121,13 +1166,13 @@ RSpec.describe API::Users, feature_category: :user_profile do optional_attributes = { confirm: true, theme_id: 2, color_scheme_id: 4 } attributes = attributes_for(:user).merge(optional_attributes) - post api('/users', admin), params: attributes + post api(path, admin, admin_mode: true), params: attributes expect(response).to have_gitlab_http_status(:created) end it "creates non-admin user" do - post api('/users', admin), params: attributes_for(:user, admin: false, can_create_group: false) + post api(path, admin, admin_mode: true), params: attributes_for(:user, admin: false, can_create_group: false) expect(response).to have_gitlab_http_status(:created) user_id = json_response['id'] new_user = User.find(user_id) @@ -1136,7 +1181,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "creates non-admin users by default" do - post api('/users', admin), params: attributes_for(:user) + post api(path, admin, admin_mode: true), params: attributes_for(:user) expect(response).to have_gitlab_http_status(:created) user_id = json_response['id'] new_user = User.find(user_id) @@ -1144,13 +1189,13 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "returns 201 Created on success" do - post api("/users", admin), params: attributes_for(:user, projects_limit: 3) + post api(path, admin, admin_mode: true), params: attributes_for(:user, projects_limit: 3) expect(response).to match_response_schema('public_api/v4/user/admin') expect(response).to have_gitlab_http_status(:created) end it 'creates non-external users by default' do - post api("/users", admin), params: attributes_for(:user) + post api(path, admin, admin_mode: true), params: attributes_for(:user) expect(response).to have_gitlab_http_status(:created) user_id = json_response['id'] @@ -1159,7 +1204,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'allows an external user to be created' do - post api("/users", admin), params: attributes_for(:user, external: true) + post api(path, admin, admin_mode: true), params: attributes_for(:user, external: true) expect(response).to have_gitlab_http_status(:created) user_id = json_response['id'] @@ -1168,7 +1213,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "creates user with reset password" do - post api('/users', admin), params: attributes_for(:user, reset_password: true).except(:password) + post api(path, admin, admin_mode: true), params: attributes_for(:user, reset_password: true).except(:password) expect(response).to have_gitlab_http_status(:created) @@ -1181,7 +1226,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it "creates user with random password" do params = attributes_for(:user, force_random_password: true) params.delete(:password) - post api('/users', admin), params: params + post api(path, admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:created) @@ -1192,7 +1237,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "creates user with private profile" do - post api('/users', admin), params: attributes_for(:user, private_profile: true) + post api(path, admin, admin_mode: true), params: attributes_for(:user, private_profile: true) expect(response).to have_gitlab_http_status(:created) @@ -1204,7 +1249,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "creates user with view_diffs_file_by_file" do - post api('/users', admin), params: attributes_for(:user, view_diffs_file_by_file: true) + post api(path, admin, admin_mode: true), params: attributes_for(:user, view_diffs_file_by_file: true) expect(response).to have_gitlab_http_status(:created) @@ -1217,7 +1262,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it "creates user with avatar" do workhorse_form_with_file( - api('/users', admin), + api(path, admin, admin_mode: true), method: :post, file_key: :avatar, params: attributes_for(:user, avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif')) @@ -1232,7 +1277,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "does not create user with invalid email" do - post api('/users', admin), + post api(path, admin, admin_mode: true), params: { email: 'invalid email', password: User.random_password, @@ -1242,22 +1287,22 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'returns 400 error if name not given' do - post api('/users', admin), params: attributes_for(:user).except(:name) + post api(path, admin, admin_mode: true), params: attributes_for(:user).except(:name) expect(response).to have_gitlab_http_status(:bad_request) end it 'returns 400 error if password not given' do - post api('/users', admin), params: attributes_for(:user).except(:password) + post api(path, admin, admin_mode: true), params: attributes_for(:user).except(:password) expect(response).to have_gitlab_http_status(:bad_request) end it 'returns 400 error if email not given' do - post api('/users', admin), params: attributes_for(:user).except(:email) + post api(path, admin, admin_mode: true), params: attributes_for(:user).except(:email) expect(response).to have_gitlab_http_status(:bad_request) end it 'returns 400 error if username not given' do - post api('/users', admin), params: attributes_for(:user).except(:username) + post api(path, admin, admin_mode: true), params: attributes_for(:user).except(:username) expect(response).to have_gitlab_http_status(:bad_request) end @@ -1265,13 +1310,13 @@ RSpec.describe API::Users, feature_category: :user_profile do optional_attributes = { theme_id: 50, color_scheme_id: 50 } attributes = attributes_for(:user).merge(optional_attributes) - post api('/users', admin), params: attributes + post api(path, admin, admin_mode: true), params: attributes expect(response).to have_gitlab_http_status(:bad_request) end it 'returns 400 error if user does not validate' do - post api('/users', admin), + post api(path, admin, admin_mode: true), params: { password: 'pass', email: 'test@example.com', @@ -1288,12 +1333,12 @@ RSpec.describe API::Users, feature_category: :user_profile do expect(json_response['message']['projects_limit']) .to eq(['must be greater than or equal to 0']) expect(json_response['message']['username']) - .to eq([Gitlab::PathRegex.namespace_format_message]) + .to match_array([Gitlab::PathRegex.namespace_format_message, Gitlab::Regex.oci_repository_path_regex_message]) end it 'tracks weak password errors' do attributes = attributes_for(:user).merge({ password: "password" }) - post api('/users', admin), params: attributes + post api(path, admin, admin_mode: true), params: attributes expect(json_response['message']['password']) .to eq(['must not contain commonly used combinations of words and letters']) @@ -1306,13 +1351,13 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "is not available for non admin users" do - post api("/users", user), params: attributes_for(:user) + post api(path, user), params: attributes_for(:user) expect(response).to have_gitlab_http_status(:forbidden) end context 'with existing user' do before do - post api('/users', admin), + post api(path, admin, admin_mode: true), params: { email: 'test@example.com', password: User.random_password, @@ -1323,7 +1368,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns 409 conflict error if user with same email exists' do expect do - post api('/users', admin), + post api(path, admin, admin_mode: true), params: { name: 'foo', email: 'test@example.com', @@ -1337,7 +1382,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns 409 conflict error if same username exists' do expect do - post api('/users', admin), + post api(path, admin, admin_mode: true), params: { name: 'foo', email: 'foo@example.com', @@ -1351,7 +1396,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns 409 conflict error if same username exists (case insensitive)' do expect do - post api('/users', admin), + post api(path, admin, admin_mode: true), params: { name: 'foo', email: 'foo@example.com', @@ -1364,7 +1409,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'creates user with new identity' do - post api("/users", admin), params: attributes_for(:user, provider: 'github', extern_uid: '67890') + post api(path, admin, admin_mode: true), params: attributes_for(:user, provider: 'github', extern_uid: '67890') expect(response).to have_gitlab_http_status(:created) expect(json_response['identities'].first['extern_uid']).to eq('67890') @@ -1378,7 +1423,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns 409 conflict error' do expect do - post api('/users', admin), + post api(path, admin, admin_mode: true), params: { name: 'foo', email: confirmed_user.email, @@ -1396,7 +1441,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns 409 conflict error' do expect do - post api('/users', admin), + post api(path, admin, admin_mode: true), params: { name: 'foo', email: unconfirmed_user.email, @@ -1416,7 +1461,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns 409 conflict error' do expect do - post api('/users', admin), + post api(path, admin, admin_mode: true), params: { name: 'foo', email: email.email, @@ -1434,7 +1479,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'does not create user' do expect do - post api('/users', admin), + post api(path, admin, admin_mode: true), params: { name: 'foo', email: email.email, @@ -1465,7 +1510,7 @@ RSpec.describe API::Users, feature_category: :user_profile do shared_examples_for 'creates the user with the value of `private_profile` based on the application setting' do specify do - post api("/users", admin), params: params + post api(path, admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:created) user = User.find_by(id: json_response['id'], private_profile: true) @@ -1479,7 +1524,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'when the attribute is overridden in params' do it 'creates the user with the value of `private_profile` same as the value of the overridden param' do - post api("/users", admin), params: params.merge(private_profile: false) + post api(path, admin, admin_mode: true), params: params.merge(private_profile: false) expect(response).to have_gitlab_http_status(:created) user = User.find_by(id: json_response['id'], private_profile: false) @@ -1497,8 +1542,14 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe "PUT /users/:id" do + let(:path) { "/users/#{user.id}" } + + it_behaves_like 'PUT request permissions for admin mode' do + let(:params) { { bio: 'new test bio' } } + end + it "returns 200 OK on success" do - put api("/users/#{user.id}", admin), params: { bio: 'new test bio' } + put api(path, admin, admin_mode: true), params: { bio: 'new test bio' } expect(response).to match_response_schema('public_api/v4/user/admin') expect(response).to have_gitlab_http_status(:ok) @@ -1506,7 +1557,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'updating password' do def update_password(user, admin, password = User.random_password) - put api("/users/#{user.id}", admin), params: { password: password } + put api("/users/#{user.id}", admin, admin_mode: true), params: { password: password } end context 'admin updates their own password' do @@ -1564,7 +1615,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "updates user with new bio" do - put api("/users/#{user.id}", admin), params: { bio: 'new test bio' } + put api(path, admin, admin_mode: true), params: { bio: 'new test bio' } expect(response).to have_gitlab_http_status(:ok) expect(json_response['bio']).to eq('new test bio') @@ -1574,7 +1625,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it "updates user with empty bio" do user.update!(bio: 'previous bio') - put api("/users/#{user.id}", admin), params: { bio: '' } + put api(path, admin, admin_mode: true), params: { bio: '' } expect(response).to have_gitlab_http_status(:ok) expect(json_response['bio']).to eq('') @@ -1582,7 +1633,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'updates user with nil bio' do - put api("/users/#{user.id}", admin), params: { bio: nil } + put api(path, admin, admin_mode: true), params: { bio: nil } expect(response).to have_gitlab_http_status(:ok) expect(json_response['bio']).to eq('') @@ -1590,7 +1641,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "updates user with organization" do - put api("/users/#{user.id}", admin), params: { organization: 'GitLab' } + put api(path, admin, admin_mode: true), params: { organization: 'GitLab' } expect(response).to have_gitlab_http_status(:ok) expect(json_response['organization']).to eq('GitLab') @@ -1599,7 +1650,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'updates user with avatar' do workhorse_form_with_file( - api("/users/#{user.id}", admin), + api(path, admin, admin_mode: true), method: :put, file_key: :avatar, params: { avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') } @@ -1615,7 +1666,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'updates user with a new email' do old_email = user.email old_notification_email = user.notification_email_or_default - put api("/users/#{user.id}", admin), params: { email: 'new@email.com' } + put api(path, admin, admin_mode: true), params: { email: 'new@email.com' } user.reload @@ -1627,7 +1678,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'skips reconfirmation when requested' do - put api("/users/#{user.id}", admin), params: { email: 'new@email.com', skip_reconfirmation: true } + put api(path, admin, admin_mode: true), params: { email: 'new@email.com', skip_reconfirmation: true } user.reload @@ -1637,7 +1688,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'updates user with their own username' do - put api("/users/#{user.id}", admin), params: { username: user.username } + put api(path, admin, admin_mode: true), params: { username: user.username } expect(response).to have_gitlab_http_status(:ok) expect(json_response['username']).to eq(user.username) @@ -1645,14 +1696,14 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "updates user's existing identity" do - put api("/users/#{ldap_user.id}", admin), params: { provider: 'ldapmain', extern_uid: '654321' } + put api("/users/#{ldap_user.id}", admin, admin_mode: true), params: { provider: 'ldapmain', extern_uid: '654321' } expect(response).to have_gitlab_http_status(:ok) expect(ldap_user.reload.identities.first.extern_uid).to eq('654321') end it 'updates user with new identity' do - put api("/users/#{user.id}", admin), params: { provider: 'github', extern_uid: 'john' } + put api(path, admin, admin_mode: true), params: { provider: 'github', extern_uid: 'john' } expect(response).to have_gitlab_http_status(:ok) expect(user.reload.identities.first.extern_uid).to eq('john') @@ -1660,14 +1711,14 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "updates admin status" do - put api("/users/#{user.id}", admin), params: { admin: true } + put api(path, admin, admin_mode: true), params: { admin: true } expect(response).to have_gitlab_http_status(:ok) expect(user.reload.admin).to eq(true) end it "updates external status" do - put api("/users/#{user.id}", admin), params: { external: true } + put api(path, admin, admin_mode: true), params: { external: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response['external']).to eq(true) @@ -1675,14 +1726,14 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "does have default values for theme and color-scheme ID" do - put api("/users/#{user.id}", admin), params: {} + put api(path, admin, admin_mode: true), params: {} expect(user.reload.theme_id).to eq(Gitlab::Themes.default.id) expect(user.reload.color_scheme_id).to eq(Gitlab::ColorSchemes.default.id) end it "updates viewing diffs file by file" do - put api("/users/#{user.id}", admin), params: { view_diffs_file_by_file: true } + put api(path, admin, admin_mode: true), params: { view_diffs_file_by_file: true } expect(response).to have_gitlab_http_status(:ok) expect(user.reload.user_preference.view_diffs_file_by_file?).to eq(true) @@ -1693,7 +1744,7 @@ RSpec.describe API::Users, feature_category: :user_profile do current_value = user.private_profile new_value = !current_value - put api("/users/#{user.id}", admin), params: { private_profile: new_value } + put api(path, admin, admin_mode: true), params: { private_profile: new_value } expect(response).to have_gitlab_http_status(:ok) expect(user.reload.private_profile).to eq(new_value) @@ -1707,7 +1758,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it "updates private_profile to value of the application setting" do user.update!(private_profile: false) - put api("/users/#{user.id}", admin), params: { private_profile: nil } + put api(path, admin, admin_mode: true), params: { private_profile: nil } expect(response).to have_gitlab_http_status(:ok) expect(user.reload.private_profile).to eq(true) @@ -1717,7 +1768,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it "does not modify private profile when field is not provided" do user.update!(private_profile: true) - put api("/users/#{user.id}", admin), params: {} + put api(path, admin, admin_mode: true), params: {} expect(response).to have_gitlab_http_status(:ok) expect(user.reload.private_profile).to eq(true) @@ -1730,7 +1781,7 @@ RSpec.describe API::Users, feature_category: :user_profile do user.update!(theme_id: theme.id, color_scheme_id: scheme.id) - put api("/users/#{user.id}", admin), params: {} + put api(path, admin, admin_mode: true), params: {} expect(response).to have_gitlab_http_status(:ok) expect(user.reload.theme_id).to eq(theme.id) @@ -1740,7 +1791,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it "does not update admin status" do admin_user = create(:admin) - put api("/users/#{admin_user.id}", admin), params: { can_create_group: false } + put api("/users/#{admin_user.id}", admin, admin_mode: true), params: { can_create_group: false } expect(response).to have_gitlab_http_status(:ok) expect(admin_user.reload.admin).to eq(true) @@ -1748,35 +1799,35 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "does not allow invalid update" do - put api("/users/#{user.id}", admin), params: { email: 'invalid email' } + put api(path, admin, admin_mode: true), params: { email: 'invalid email' } expect(response).to have_gitlab_http_status(:bad_request) expect(user.reload.email).not_to eq('invalid email') end it "updates theme id" do - put api("/users/#{user.id}", admin), params: { theme_id: 5 } + put api(path, admin, admin_mode: true), params: { theme_id: 5 } expect(response).to have_gitlab_http_status(:ok) expect(user.reload.theme_id).to eq(5) end it "does not update invalid theme id" do - put api("/users/#{user.id}", admin), params: { theme_id: 50 } + put api(path, admin, admin_mode: true), params: { theme_id: 50 } expect(response).to have_gitlab_http_status(:bad_request) expect(user.reload.theme_id).not_to eq(50) end it "updates color scheme id" do - put api("/users/#{user.id}", admin), params: { color_scheme_id: 5 } + put api(path, admin, admin_mode: true), params: { color_scheme_id: 5 } expect(response).to have_gitlab_http_status(:ok) expect(user.reload.color_scheme_id).to eq(5) end it "does not update invalid color scheme id" do - put api("/users/#{user.id}", admin), params: { color_scheme_id: 50 } + put api(path, admin, admin_mode: true), params: { color_scheme_id: 50 } expect(response).to have_gitlab_http_status(:bad_request) expect(user.reload.color_scheme_id).not_to eq(50) @@ -1785,7 +1836,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'when the current user is not an admin' do it "is not available" do expect do - put api("/users/#{user.id}", user), params: attributes_for(:user) + put api(path, user), params: attributes_for(:user) end.not_to change { user.reload.attributes } expect(response).to have_gitlab_http_status(:forbidden) @@ -1793,20 +1844,20 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "returns 404 for non-existing user" do - put api("/users/0", admin), params: { bio: 'update should fail' } + put api("/users/#{non_existing_record_id}", admin, admin_mode: true), params: { bio: 'update should fail' } expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') end it "returns a 404 if invalid ID" do - put api("/users/ASDF", admin) + put api("/users/ASDF", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end it 'returns 400 error if user does not validate' do - put api("/users/#{user.id}", admin), + put api(path, admin, admin_mode: true), params: { password: 'pass', email: 'test@example.com', @@ -1823,30 +1874,30 @@ RSpec.describe API::Users, feature_category: :user_profile do expect(json_response['message']['projects_limit']) .to eq(['must be greater than or equal to 0']) expect(json_response['message']['username']) - .to eq([Gitlab::PathRegex.namespace_format_message]) + .to match_array([Gitlab::PathRegex.namespace_format_message, Gitlab::Regex.oci_repository_path_regex_message]) end it 'returns 400 if provider is missing for identity update' do - put api("/users/#{omniauth_user.id}", admin), params: { extern_uid: '654321' } + put api("/users/#{omniauth_user.id}", admin, admin_mode: true), params: { extern_uid: '654321' } expect(response).to have_gitlab_http_status(:bad_request) end it 'returns 400 if external UID is missing for identity update' do - put api("/users/#{omniauth_user.id}", admin), params: { provider: 'ldap' } + put api("/users/#{omniauth_user.id}", admin, admin_mode: true), params: { provider: 'ldap' } expect(response).to have_gitlab_http_status(:bad_request) end context "with existing user" do before do - post api("/users", admin), params: { email: 'test@example.com', password: User.random_password, username: 'test', name: 'test' } - post api("/users", admin), params: { email: 'foo@bar.com', password: User.random_password, username: 'john', name: 'john' } + post api("/users", admin, admin_mode: true), params: { email: 'test@example.com', password: User.random_password, username: 'test', name: 'test' } + post api("/users", admin, admin_mode: true), params: { email: 'foo@bar.com', password: User.random_password, username: 'john', name: 'john' } @user = User.all.last end it 'returns 409 conflict error if email address exists' do - put api("/users/#{@user.id}", admin), params: { email: 'test@example.com' } + put api("/users/#{@user.id}", admin, admin_mode: true), params: { email: 'test@example.com' } expect(response).to have_gitlab_http_status(:conflict) expect(@user.reload.email).to eq(@user.email) @@ -1854,7 +1905,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns 409 conflict error if username taken' do @user_id = User.all.last.id - put api("/users/#{@user.id}", admin), params: { username: 'test' } + put api("/users/#{@user.id}", admin, admin_mode: true), params: { username: 'test' } expect(response).to have_gitlab_http_status(:conflict) expect(@user.reload.username).to eq(@user.username) @@ -1862,7 +1913,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns 409 conflict error if username taken (case insensitive)' do @user_id = User.all.last.id - put api("/users/#{@user.id}", admin), params: { username: 'TEST' } + put api("/users/#{@user.id}", admin, admin_mode: true), params: { username: 'TEST' } expect(response).to have_gitlab_http_status(:conflict) expect(@user.reload.username).to eq(@user.username) @@ -1874,7 +1925,7 @@ RSpec.describe API::Users, feature_category: :user_profile do let!(:confirmed_user) { create(:user, email: 'foo@example.com') } it 'returns 409 conflict error' do - put api("/users/#{user.id}", admin), params: { email: confirmed_user.email } + put api(path, admin, admin_mode: true), params: { email: confirmed_user.email } expect(response).to have_gitlab_http_status(:conflict) expect(user.reload.email).not_to eq(confirmed_user.email) @@ -1885,7 +1936,7 @@ RSpec.describe API::Users, feature_category: :user_profile do let!(:unconfirmed_user) { create(:user, :unconfirmed, email: 'foo@example.com') } it 'returns 409 conflict error' do - put api("/users/#{user.id}", admin), params: { email: unconfirmed_user.email } + put api(path, admin, admin_mode: true), params: { email: unconfirmed_user.email } expect(response).to have_gitlab_http_status(:conflict) expect(user.reload.email).not_to eq(unconfirmed_user.email) @@ -1898,7 +1949,7 @@ RSpec.describe API::Users, feature_category: :user_profile do let!(:email) { create(:email, :confirmed, email: 'foo@example.com') } it 'returns 409 conflict error' do - put api("/users/#{user.id}", admin), params: { email: email.email } + put api(path, admin, admin_mode: true), params: { email: email.email } expect(response).to have_gitlab_http_status(:conflict) expect(user.reload.email).not_to eq(email.email) @@ -1909,7 +1960,7 @@ RSpec.describe API::Users, feature_category: :user_profile do let!(:email) { create(:email, email: 'foo@example.com') } it 'does not update email' do - put api("/users/#{user.id}", admin), params: { email: email.email } + put api(path, admin, admin_mode: true), params: { email: email.email } expect(response).to have_gitlab_http_status(:bad_request) expect(user.reload.email).not_to eq(email.email) @@ -1921,6 +1972,7 @@ RSpec.describe API::Users, feature_category: :user_profile 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(:path) { "/user/#{user.id}/credit_card_validation" } let(:params) do { credit_card_validated_at: credit_card_validated_time, @@ -1932,25 +1984,27 @@ RSpec.describe API::Users, feature_category: :user_profile do } end + it_behaves_like 'PUT request permissions for admin mode' + context 'when unauthenticated' do it 'returns authentication error' do - put api("/user/#{user.id}/credit_card_validation"), params: {} + put api(path), params: {} expect(response).to have_gitlab_http_status(:unauthorized) end end 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: params + it "does not allow updating user's credit card validation" do + put api(path, user), params: params expect(response).to have_gitlab_http_status(:forbidden) end end 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: params + it "updates user's credit card validation" do + put api(path, admin, admin_mode: true), params: params user.reload @@ -1965,13 +2019,13 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "returns 400 error if credit_card_validated_at is missing" do - put api("/user/#{user.id}/credit_card_validation", admin), params: {} + put api(path, admin, admin_mode: true), params: {} expect(response).to have_gitlab_http_status(:bad_request) end it 'returns 404 error if user not found' do - put api("/user/#{non_existing_record_id}/credit_card_validation", admin), params: params + put api("/user/#{non_existing_record_id}/credit_card_validation", admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') @@ -1981,10 +2035,13 @@ RSpec.describe API::Users, feature_category: :user_profile do describe "DELETE /users/:id/identities/:provider" do let(:test_user) { create(:omniauth_user, provider: 'ldapmain') } + let(:path) { "/users/#{test_user.id}/identities/ldapmain" } + + it_behaves_like 'DELETE request permissions for admin mode' context 'when unauthenticated' do it 'returns authentication error' do - delete api("/users/#{test_user.id}/identities/ldapmain") + delete api(path) expect(response).to have_gitlab_http_status(:unauthorized) end @@ -1993,24 +2050,24 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'when authenticated' do it 'deletes identity of given provider' do expect do - delete api("/users/#{test_user.id}/identities/ldapmain", admin) + delete api(path, admin, admin_mode: true) end.to change { test_user.identities.count }.by(-1) expect(response).to have_gitlab_http_status(:no_content) end it_behaves_like '412 response' do - let(:request) { api("/users/#{test_user.id}/identities/ldapmain", admin) } + let(:request) { api(path, admin, admin_mode: true) } end it 'returns 404 error if user not found' do - delete api("/users/0/identities/ldapmain", admin) + delete api("/users/#{non_existing_record_id}/identities/ldapmain", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') end it 'returns 404 error if identity not found' do - delete api("/users/#{test_user.id}/identities/saml", admin) + delete api("/users/#{test_user.id}/identities/saml", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Identity Not Found') @@ -2019,25 +2076,31 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe "POST /users/:id/keys" do + let(:path) { "/users/#{user.id}/keys" } + + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { attributes_for(:key, usage_type: :signing) } + end + it "does not create invalid ssh key" do - post api("/users/#{user.id}/keys", admin), params: { title: "invalid key" } + post api(path, admin, admin_mode: true), params: { title: "invalid key" } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq('key is missing') end it 'does not create key without title' do - post api("/users/#{user.id}/keys", admin), params: { key: 'some key' } + post api(path, admin, admin_mode: true), params: { key: 'some key' } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq('title is missing') end - it "creates ssh key", :aggregate_failures do + it "creates ssh key" do key_attrs = attributes_for(:key, usage_type: :signing) expect do - post api("/users/#{user.id}/keys", admin), params: key_attrs + post api(path, admin, admin_mode: true), params: key_attrs end.to change { user.keys.count }.by(1) expect(response).to have_gitlab_http_status(:created) @@ -2052,20 +2115,21 @@ RSpec.describe API::Users, feature_category: :user_profile do optional_attributes = { expires_at: 3.weeks.from_now } attributes = attributes_for(:key).merge(optional_attributes) - post api("/users/#{user.id}/keys", admin), params: attributes + post api(path, admin, admin_mode: true), params: attributes expect(response).to have_gitlab_http_status(:created) expect(json_response['expires_at'].to_date).to eq(optional_attributes[:expires_at].to_date) end it "returns 400 for invalid ID" do - post api("/users/0/keys", admin) + post api("/users/#{non_existing_record_id}/keys", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) end end describe 'GET /users/:id/project_deploy_keys' do let(:project) { create(:project) } + let(:path) { "/users/#{user.id}/project_deploy_keys" } before do project.add_maintainer(user) @@ -2082,7 +2146,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'returns array of project deploy keys with pagination' do - get api("/users/#{user.id}/project_deploy_keys", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -2094,7 +2158,7 @@ RSpec.describe API::Users, feature_category: :user_profile do dev_user = create(:user) project.add_developer(dev_user) - get api("/users/#{user.id}/project_deploy_keys", dev_user) + get api(path, dev_user) expect(response).to have_gitlab_http_status(:forbidden) expect(json_response['message']).to eq('403 Forbidden - No common authorized project found') @@ -2113,7 +2177,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'when no common projects for user and current_user' do it 'forbids' do - get api("/users/#{user.id}/project_deploy_keys", second_user) + get api(path, second_user) expect(response).to have_gitlab_http_status(:forbidden) expect(json_response['message']).to eq('403 Forbidden - No common authorized project found') @@ -2125,11 +2189,13 @@ RSpec.describe API::Users, feature_category: :user_profile do project.add_maintainer(second_user) end + let(:path) { "/users/#{second_user.id}/project_deploy_keys" } + it 'lists only common project keys' do expect(second_user.project_deploy_keys).to contain_exactly( project.deploy_keys.first, second_project.deploy_keys.first) - get api("/users/#{second_user.id}/project_deploy_keys", user) + get api(path, user) expect(json_response.count).to eq(1) expect(json_response.first['key']).to eq(project.deploy_keys.first.key) @@ -2144,7 +2210,7 @@ RSpec.describe API::Users, feature_category: :user_profile do create(:deploy_key, user: second_user) create(:deploy_key, user: third_user) - get api("/users/#{second_user.id}/project_deploy_keys", third_user) + get api(path, third_user) expect(json_response.count).to eq(2) expect([json_response.first['key'], json_response.second['key']]).to contain_exactly( @@ -2155,14 +2221,14 @@ RSpec.describe API::Users, feature_category: :user_profile do second_project.add_maintainer(user) control_count = ActiveRecord::QueryRecorder.new do - get api("/users/#{second_user.id}/project_deploy_keys", user) + get api(path, user) end.count deploy_key = create(:deploy_key, user: second_user) create(:deploy_keys_project, project: second_project, deploy_key_id: deploy_key.id) expect do - get api("/users/#{second_user.id}/project_deploy_keys", user) + get api(path, user) end.not_to exceed_query_limit(control_count) end end @@ -2170,6 +2236,10 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe 'GET /user/:id/keys' do + subject(:request) { get api(path) } + + let(:path) { "/users/#{user.id}/keys" } + it 'returns 404 for non-existing user' do get api("/users/#{non_existing_record_id}/keys") @@ -2180,7 +2250,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns array of ssh keys' do user.keys << key - get api("/users/#{user.id}/keys") + request expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -2190,7 +2260,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns array of ssh keys with comments replaced with'\ 'a simple identifier of username + hostname' do - get api("/users/#{user.id}/keys") + request expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -2202,24 +2272,26 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'N+1 queries' do before do - get api("/users/#{user.id}/keys") + request end it 'avoids N+1 queries', :request_store do control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do - get api("/users/#{user.id}/keys") + request end.count create_list(:key, 2, user: user) expect do - get api("/users/#{user.id}/keys") + request end.not_to exceed_all_query_limit(control_count) end end end describe 'GET /user/:user_id/keys' do + let(:path) { "/users/#{user.username}/keys" } + it 'returns 404 for non-existing user' do get api("/users/#{non_existing_record_id}/keys") @@ -2230,7 +2302,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns array of ssh keys' do user.keys << key - get api("/users/#{user.username}/keys") + get api(path) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -2240,25 +2312,27 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe 'GET /user/:id/keys/:key_id' do - it 'gets existing key', :aggregate_failures do + let(:path) { "/users/#{user.id}/keys/#{key.id}" } + + it 'gets existing key' do user.keys << key - get api("/users/#{user.id}/keys/#{key.id}") + get api(path) expect(response).to have_gitlab_http_status(:ok) expect(json_response['title']).to eq(key.title) end - it 'returns 404 error if user not found', :aggregate_failures do + it 'returns 404 error if user not found' do user.keys << key - get api("/users/0/keys/#{key.id}") + get api("/users/#{non_existing_record_id}/keys/#{key.id}") expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') end - it 'returns 404 error if key not found', :aggregate_failures do + it 'returns 404 error if key not found' do get api("/users/#{user.id}/keys/#{non_existing_record_id}") expect(response).to have_gitlab_http_status(:not_found) @@ -2267,6 +2341,10 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe 'DELETE /user/:id/keys/:key_id' do + let(:path) { "/users/#{user.id}/keys/#{key.id}" } + + it_behaves_like 'DELETE request permissions for admin mode' + context 'when unauthenticated' do it 'returns authentication error' do delete api("/users/#{user.id}/keys/#{non_existing_record_id}") @@ -2279,26 +2357,26 @@ RSpec.describe API::Users, feature_category: :user_profile do user.keys << key expect do - delete api("/users/#{user.id}/keys/#{key.id}", admin) + delete api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:no_content) end.to change { user.keys.count }.by(-1) end it_behaves_like '412 response' do - let(:request) { api("/users/#{user.id}/keys/#{key.id}", admin) } + let(:request) { api(path, admin, admin_mode: true) } end it 'returns 404 error if user not found' do user.keys << key - delete api("/users/0/keys/#{key.id}", admin) + delete api("/users/#{non_existing_record_id}/keys/#{key.id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') end it 'returns 404 error if key not foud' do - delete api("/users/#{user.id}/keys/#{non_existing_record_id}", admin) + delete api("/users/#{user.id}/keys/#{non_existing_record_id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Key Not Found') end @@ -2306,8 +2384,14 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe 'POST /users/:id/gpg_keys' do + let(:path) { "/users/#{user.id}/gpg_keys" } + + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { attributes_for :gpg_key, key: GpgHelpers::User2.public_key } + end + it 'does not create invalid GPG key' do - post api("/users/#{user.id}/gpg_keys", admin) + post api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq('key is missing') @@ -2317,22 +2401,24 @@ RSpec.describe API::Users, feature_category: :user_profile do key_attrs = attributes_for :gpg_key, key: GpgHelpers::User2.public_key expect do - post api("/users/#{user.id}/gpg_keys", admin), params: key_attrs + post api(path, admin, admin_mode: true), params: key_attrs expect(response).to have_gitlab_http_status(:created) end.to change { user.gpg_keys.count }.by(1) end it 'returns 400 for invalid ID' do - post api('/users/0/gpg_keys', admin) + post api("/users/#{non_existing_record_id}/gpg_keys", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) end end describe 'GET /user/:id/gpg_keys' do + let(:path) { "/users/#{user.id}/gpg_keys" } + it 'returns 404 for non-existing user' do - get api('/users/0/gpg_keys') + get api("/users/#{non_existing_record_id}/gpg_keys") expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') @@ -2341,7 +2427,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns array of GPG keys' do user.gpg_keys << gpg_key - get api("/users/#{user.id}/gpg_keys") + get api(path) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -2351,15 +2437,17 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe 'GET /user/:id/gpg_keys/:key_id' do + let(:path) { "/users/#{user.id}/gpg_keys/#{gpg_key.id}" } + it 'returns 404 for non-existing user' do - get api('/users/0/gpg_keys/1') + get api("/users/#{non_existing_record_id}/gpg_keys/1") expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') end it 'returns 404 for non-existing key' do - get api("/users/#{user.id}/gpg_keys/0") + get api("/users/#{user.id}/gpg_keys/#{non_existing_record_id}") expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 GPG Key Not Found') @@ -2368,7 +2456,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns a single GPG key' do user.gpg_keys << gpg_key - get api("/users/#{user.id}/gpg_keys/#{gpg_key.id}") + get api(path) expect(response).to have_gitlab_http_status(:ok) expect(json_response['key']).to eq(gpg_key.key) @@ -2376,6 +2464,10 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe 'DELETE /user/:id/gpg_keys/:key_id' do + let(:path) { "/users/#{user.id}/gpg_keys/#{gpg_key.id}" } + + it_behaves_like 'DELETE request permissions for admin mode' + context 'when unauthenticated' do it 'returns authentication error' do delete api("/users/#{user.id}/keys/#{non_existing_record_id}") @@ -2389,7 +2481,7 @@ RSpec.describe API::Users, feature_category: :user_profile do user.gpg_keys << gpg_key expect do - delete api("/users/#{user.id}/gpg_keys/#{gpg_key.id}", admin) + delete api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:no_content) end.to change { user.gpg_keys.count }.by(-1) @@ -2398,14 +2490,14 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns 404 error if user not found' do user.keys << key - delete api("/users/0/gpg_keys/#{gpg_key.id}", admin) + delete api("/users/#{non_existing_record_id}/gpg_keys/#{gpg_key.id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') end it 'returns 404 error if key not foud' do - delete api("/users/#{user.id}/gpg_keys/#{non_existing_record_id}", admin) + delete api("/users/#{user.id}/gpg_keys/#{non_existing_record_id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 GPG Key Not Found') @@ -2414,6 +2506,13 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe 'POST /user/:id/gpg_keys/:key_id/revoke' do + let(:path) { "/users/#{user.id}/gpg_keys/#{gpg_key.id}/revoke" } + + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { {} } + let(:success_status_code) { :accepted } + end + context 'when unauthenticated' do it 'returns authentication error' do post api("/users/#{user.id}/gpg_keys/#{non_existing_record_id}/revoke") @@ -2427,7 +2526,7 @@ RSpec.describe API::Users, feature_category: :user_profile do user.gpg_keys << gpg_key expect do - post api("/users/#{user.id}/gpg_keys/#{gpg_key.id}/revoke", admin) + post api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:accepted) end.to change { user.gpg_keys.count }.by(-1) @@ -2436,14 +2535,14 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns 404 error if user not found' do user.gpg_keys << gpg_key - post api("/users/0/gpg_keys/#{gpg_key.id}/revoke", admin) + post api("/users/#{non_existing_record_id}/gpg_keys/#{gpg_key.id}/revoke", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') end it 'returns 404 error if key not foud' do - post api("/users/#{user.id}/gpg_keys/#{non_existing_record_id}/revoke", admin) + post api("/users/#{user.id}/gpg_keys/#{non_existing_record_id}/revoke", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 GPG Key Not Found') @@ -2452,8 +2551,19 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe "POST /users/:id/emails", :mailer do + let(:path) { "/users/#{user.id}/emails" } + + it_behaves_like 'POST request permissions for admin mode' do + before do + email_attrs[:skip_confirmation] = true + end + + let(:email_attrs) { attributes_for :email } + let(:params) { email_attrs } + end + it "does not create invalid email" do - post api("/users/#{user.id}/emails", admin), params: {} + post api(path, admin, admin_mode: true), params: {} expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq('email is missing') @@ -2464,7 +2574,7 @@ RSpec.describe API::Users, feature_category: :user_profile do perform_enqueued_jobs do expect do - post api("/users/#{user.id}/emails", admin), params: email_attrs + post api(path, admin, admin_mode: true), params: email_attrs end.to change { user.emails.count }.by(1) end @@ -2473,7 +2583,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "returns a 400 for invalid ID" do - post api("/users/0/emails", admin) + post api("/users/#{non_existing_record_id}/emails", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) end @@ -2482,7 +2592,7 @@ RSpec.describe API::Users, feature_category: :user_profile do email_attrs = attributes_for :email email_attrs[:skip_confirmation] = true - post api("/users/#{user.id}/emails", admin), params: email_attrs + post api(path, admin, admin_mode: true), params: email_attrs expect(response).to have_gitlab_http_status(:created) @@ -2494,7 +2604,7 @@ RSpec.describe API::Users, feature_category: :user_profile do let!(:confirmed_user) { create(:user, email: 'foo@example.com') } it 'returns 400 error' do - post api("/users/#{user.id}/emails", admin), params: { email: confirmed_user.email } + post api(path, admin, admin_mode: true), params: { email: confirmed_user.email } expect(response).to have_gitlab_http_status(:bad_request) end @@ -2504,7 +2614,7 @@ RSpec.describe API::Users, feature_category: :user_profile do let!(:unconfirmed_user) { create(:user, :unconfirmed, email: 'foo@example.com') } it 'returns 400 error' do - post api("/users/#{user.id}/emails", admin), params: { email: unconfirmed_user.email } + post api(path, admin, admin_mode: true), params: { email: unconfirmed_user.email } expect(response).to have_gitlab_http_status(:bad_request) end @@ -2516,7 +2626,7 @@ RSpec.describe API::Users, feature_category: :user_profile do let!(:email) { create(:email, :confirmed, email: 'foo@example.com') } it 'returns 400 error' do - post api("/users/#{user.id}/emails", admin), params: { email: email.email } + post api(path, admin, admin_mode: true), params: { email: email.email } expect(response).to have_gitlab_http_status(:bad_request) end @@ -2526,7 +2636,7 @@ RSpec.describe API::Users, feature_category: :user_profile do let!(:email) { create(:email, email: 'foo@example.com') } it 'returns 400 error' do - post api("/users/#{user.id}/emails", admin), params: { email: email.email } + post api(path, admin, admin_mode: true), params: { email: email.email } expect(response).to have_gitlab_http_status(:bad_request) end @@ -2535,16 +2645,18 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe 'GET /user/:id/emails' do + let(:path) { "/users/#{user.id}/emails" } + context 'when unauthenticated' do it 'returns authentication error' do - get api("/users/#{user.id}/emails") + get api(path) expect(response).to have_gitlab_http_status(:unauthorized) end end context 'when authenticated' do it 'returns 404 for non-existing user' do - get api('/users/0/emails', admin) + get api("/users/#{non_existing_record_id}/emails", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') end @@ -2552,7 +2664,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns array of emails' do user.emails << email - get api("/users/#{user.id}/emails", admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -2562,7 +2674,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "returns a 404 for invalid ID" do - get api("/users/ASDF/emails", admin) + get api("/users/ASDF/emails", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end @@ -2570,6 +2682,10 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe 'DELETE /user/:id/emails/:email_id' do + let(:path) { "/users/#{user.id}/emails/#{email.id}" } + + it_behaves_like 'DELETE request permissions for admin mode' + context 'when unauthenticated' do it 'returns authentication error' do delete api("/users/#{user.id}/emails/#{non_existing_record_id}") @@ -2582,26 +2698,26 @@ RSpec.describe API::Users, feature_category: :user_profile do user.emails << email expect do - delete api("/users/#{user.id}/emails/#{email.id}", admin) + delete api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:no_content) end.to change { user.emails.count }.by(-1) end it_behaves_like '412 response' do - let(:request) { api("/users/#{user.id}/emails/#{email.id}", admin) } + subject(:request) { api(path, admin, admin_mode: true) } end it 'returns 404 error if user not found' do user.emails << email - delete api("/users/0/emails/#{email.id}", admin) + delete api("/users/#{non_existing_record_id}/emails/#{email.id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') end it 'returns 404 error if email not foud' do - delete api("/users/#{user.id}/emails/#{non_existing_record_id}", admin) + delete api("/users/#{user.id}/emails/#{non_existing_record_id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Email Not Found') end @@ -2616,9 +2732,12 @@ RSpec.describe API::Users, feature_category: :user_profile do describe "DELETE /users/:id" do let_it_be(:issue) { create(:issue, author: user) } + let(:path) { "/users/#{user.id}" } + + it_behaves_like 'DELETE request permissions for admin mode' it "deletes user", :sidekiq_inline do - perform_enqueued_jobs { delete api("/users/#{user.id}", admin) } + perform_enqueued_jobs { delete api(path, admin, admin_mode: true) } expect(response).to have_gitlab_http_status(:no_content) expect(Users::GhostUserMigration.where(user: user, @@ -2630,14 +2749,14 @@ RSpec.describe API::Users, feature_category: :user_profile do context "hard delete disabled" do it "does not delete user" do - perform_enqueued_jobs { delete api("/users/#{user.id}", admin) } + perform_enqueued_jobs { delete api(path, admin, admin_mode: true) } expect(response).to have_gitlab_http_status(:conflict) end end context "hard delete enabled" do it "delete user and group", :sidekiq_inline do - perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) } + perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin, admin_mode: true) } expect(response).to have_gitlab_http_status(:no_content) expect(Group.exists?(group.id)).to be_falsy end @@ -2652,7 +2771,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "delete only user", :sidekiq_inline do - perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) } + perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin, admin_mode: true) } expect(response).to have_gitlab_http_status(:no_content) expect(Group.exists?(subgroup.id)).to be_truthy end @@ -2661,34 +2780,34 @@ RSpec.describe API::Users, feature_category: :user_profile do end it_behaves_like '412 response' do - let(:request) { api("/users/#{user.id}", admin) } + let(:request) { api(path, admin, admin_mode: true) } end it "does not delete for unauthenticated user" do - perform_enqueued_jobs { delete api("/users/#{user.id}") } + perform_enqueued_jobs { delete api(path) } expect(response).to have_gitlab_http_status(:unauthorized) end it "is not available for non admin users" do - perform_enqueued_jobs { delete api("/users/#{user.id}", user) } + perform_enqueued_jobs { delete api(path, user) } expect(response).to have_gitlab_http_status(:forbidden) end it "returns 404 for non-existing user" do - perform_enqueued_jobs { delete api("/users/0", admin) } + perform_enqueued_jobs { delete api("/users/#{non_existing_record_id}", admin, admin_mode: true) } expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') end it "returns a 404 for invalid ID" do - perform_enqueued_jobs { delete api("/users/ASDF", admin) } + perform_enqueued_jobs { delete api("/users/ASDF", admin, admin_mode: true) } expect(response).to have_gitlab_http_status(:not_found) end context "hard delete disabled" do it "moves contributions to the ghost user", :sidekiq_might_not_need_inline do - perform_enqueued_jobs { delete api("/users/#{user.id}", admin) } + perform_enqueued_jobs { delete api(path, admin, admin_mode: true) } expect(response).to have_gitlab_http_status(:no_content) expect(issue.reload).to be_persisted @@ -2700,7 +2819,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context "hard delete enabled" do it "removes contributions", :sidekiq_might_not_need_inline do - perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) } + perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin, admin_mode: true) } expect(response).to have_gitlab_http_status(:no_content) expect(Users::GhostUserMigration.where(user: user, @@ -2711,6 +2830,8 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe "GET /user" do + let(:path) { '/user' } + shared_examples 'get user info' do |version| context 'with regular user' do context 'with personal access token' do @@ -2724,7 +2845,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'returns current user without private token when sudo not defined' do - get api("/user", user, version: version) + get api(path, user, version: version) expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('public_api/v4/user/public') @@ -2732,7 +2853,6 @@ RSpec.describe API::Users, feature_category: :user_profile do end context "scopes" do - let(:path) { "/user" } let(:api_call) { method(:api) } include_examples 'allows the "read_user" scope', version @@ -2740,7 +2860,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end context 'with admin' do - let(:admin_personal_access_token) { create(:personal_access_token, user: admin).token } + let(:admin_personal_access_token) { create(:personal_access_token, :admin_mode, user: admin).token } context 'with personal access token' do it 'returns 403 without private token when sudo defined' do @@ -2761,7 +2881,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'with unauthenticated user' do it "returns 401 error if user is unauthenticated" do - get api("/user", version: version) + get api(path, version: version) expect(response).to have_gitlab_http_status(:unauthorized) end @@ -2773,9 +2893,11 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe "GET /user/preferences" do + let(:path) { '/user/preferences' } + context "when unauthenticated" do it "returns authentication error" do - get api("/user/preferences") + get api(path) expect(response).to have_gitlab_http_status(:unauthorized) end end @@ -2786,7 +2908,7 @@ RSpec.describe API::Users, feature_category: :user_profile do user.user_preference.show_whitespace_in_diffs = true user.save! - get api("/user/preferences", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response["view_diffs_file_by_file"]).to eq(user.user_preference.view_diffs_file_by_file) @@ -2796,6 +2918,10 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe "GET /user/keys" do + subject(:request) { get api(path, user) } + + let(:path) { "/user/keys" } + context "when unauthenticated" do it "returns authentication error" do get api("/user/keys") @@ -2807,7 +2933,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it "returns array of ssh keys" do user.keys << key - get api("/user/keys", user) + request expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -2817,7 +2943,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns array of ssh keys with comments replaced with'\ 'a simple identifier of username + hostname' do - get api("/user/keys", user) + request expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -2829,24 +2955,23 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'N+1 queries' do before do - get api("/user/keys", user) + request end it 'avoids N+1 queries', :request_store do control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do - get api("/user/keys", user) + request end.count create_list(:key, 2, user: user) expect do - get api("/user/keys", user) + request end.not_to exceed_all_query_limit(control_count) end end context "scopes" do - let(:path) { "/user/keys" } let(:api_call) { method(:api) } include_examples 'allows the "read_user" scope' @@ -2855,16 +2980,18 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe "GET /user/keys/:key_id" do + let(:path) { "/user/keys/#{key.id}" } + it "returns single key" do user.keys << key - get api("/user/keys/#{key.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response["title"]).to eq(key.title) end it 'exposes SSH key comment as a simple identifier of username + hostname' do - get api("/user/keys/#{key.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response['key']).to include("#{key.user_name} (#{Gitlab.config.gitlab.host})") @@ -2881,19 +3008,18 @@ RSpec.describe API::Users, feature_category: :user_profile do user.keys << key admin - get api("/user/keys/#{key.id}", admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Key Not Found') end it "returns 404 for invalid ID" do - get api("/users/keys/ASDF", admin) + get api("/users/keys/ASDF", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end context "scopes" do - let(:path) { "/user/keys/#{key.id}" } let(:api_call) { method(:api) } include_examples 'allows the "read_user" scope' @@ -2901,11 +3027,13 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe "POST /user/keys" do - it "creates ssh key", :aggregate_failures do + let(:path) { "/user/keys" } + + it "creates ssh key" do key_attrs = attributes_for(:key, usage_type: :signing) expect do - post api("/user/keys", user), params: key_attrs + post api(path, user), params: key_attrs end.to change { user.keys.count }.by(1) expect(response).to have_gitlab_http_status(:created) @@ -2920,19 +3048,19 @@ RSpec.describe API::Users, feature_category: :user_profile do optional_attributes = { expires_at: 3.weeks.from_now } attributes = attributes_for(:key).merge(optional_attributes) - post api("/user/keys", user), params: attributes + post api(path, user), params: attributes expect(response).to have_gitlab_http_status(:created) expect(json_response['expires_at'].to_date).to eq(optional_attributes[:expires_at].to_date) end it "returns a 401 error if unauthorized" do - post api("/user/keys"), params: { title: 'some title', key: 'some key' } + post api(path), params: { title: 'some title', key: 'some key' } expect(response).to have_gitlab_http_status(:unauthorized) end it "does not create ssh key without key" do - post api("/user/keys", user), params: { title: 'title' } + post api(path, user), params: { title: 'title' } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq('key is missing') @@ -2946,24 +3074,26 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "does not create ssh key without title" do - post api("/user/keys", user), params: { key: "somekey" } + post api(path, user), params: { key: "somekey" } expect(response).to have_gitlab_http_status(:bad_request) end end describe "DELETE /user/keys/:key_id" do + let(:path) { "/user/keys/#{key.id}" } + it "deletes existed key" do user.keys << key expect do - delete api("/user/keys/#{key.id}", user) + delete api(path, user) expect(response).to have_gitlab_http_status(:no_content) end.to change { user.keys.count }.by(-1) end it_behaves_like '412 response' do - let(:request) { api("/user/keys/#{key.id}", user) } + let(:request) { api(path, user) } end it "returns 404 if key ID not found" do @@ -2976,21 +3106,23 @@ RSpec.describe API::Users, feature_category: :user_profile do it "returns 401 error if unauthorized" do user.keys << key - delete api("/user/keys/#{key.id}") + delete api(path) expect(response).to have_gitlab_http_status(:unauthorized) end it "returns a 404 for invalid ID" do - delete api("/users/keys/ASDF", admin) + delete api("/users/keys/ASDF", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end end describe 'GET /user/gpg_keys' do + let(:path) { '/user/gpg_keys' } + context 'when unauthenticated' do it 'returns authentication error' do - get api('/user/gpg_keys') + get api(path) expect(response).to have_gitlab_http_status(:unauthorized) end @@ -3000,7 +3132,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns array of GPG keys' do user.gpg_keys << gpg_key - get api('/user/gpg_keys', user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -3009,7 +3141,6 @@ RSpec.describe API::Users, feature_category: :user_profile do end context 'scopes' do - let(:path) { '/user/gpg_keys' } let(:api_call) { method(:api) } include_examples 'allows the "read_user" scope' @@ -3018,10 +3149,12 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe 'GET /user/gpg_keys/:key_id' do + let(:path) { "/user/gpg_keys/#{gpg_key.id}" } + it 'returns a single key' do user.gpg_keys << gpg_key - get api("/user/gpg_keys/#{gpg_key.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response['key']).to eq(gpg_key.key) @@ -3037,20 +3170,19 @@ RSpec.describe API::Users, feature_category: :user_profile do it "returns 404 error if admin accesses user's GPG key" do user.gpg_keys << gpg_key - get api("/user/gpg_keys/#{gpg_key.id}", admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 GPG Key Not Found') end it 'returns 404 for invalid ID' do - get api('/users/gpg_keys/ASDF', admin) + get api('/users/gpg_keys/ASDF', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end context 'scopes' do - let(:path) { "/user/gpg_keys/#{gpg_key.id}" } let(:api_call) { method(:api) } include_examples 'allows the "read_user" scope' @@ -3058,24 +3190,26 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe 'POST /user/gpg_keys' do + let(:path) { '/user/gpg_keys' } + it 'creates a GPG key' do key_attrs = attributes_for :gpg_key, key: GpgHelpers::User2.public_key expect do - post api('/user/gpg_keys', user), params: key_attrs + post api(path, user), params: key_attrs expect(response).to have_gitlab_http_status(:created) end.to change { user.gpg_keys.count }.by(1) end it 'returns a 401 error if unauthorized' do - post api('/user/gpg_keys'), params: { key: 'some key' } + post api(path), params: { key: 'some key' } expect(response).to have_gitlab_http_status(:unauthorized) end it 'does not create GPG key without key' do - post api('/user/gpg_keys', user) + post api(path, user) expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq('key is missing') @@ -3109,18 +3243,20 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'returns a 404 for invalid ID' do - post api('/users/gpg_keys/ASDF/revoke', admin) + post api('/users/gpg_keys/ASDF/revoke', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end end describe 'DELETE /user/gpg_keys/:key_id' do + let(:path) { "/user/gpg_keys/#{gpg_key.id}" } + it 'deletes existing GPG key' do user.gpg_keys << gpg_key expect do - delete api("/user/gpg_keys/#{gpg_key.id}", user) + delete api(path, user) expect(response).to have_gitlab_http_status(:no_content) end.to change { user.gpg_keys.count }.by(-1) @@ -3136,22 +3272,24 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'returns 401 error if unauthorized' do user.gpg_keys << gpg_key - delete api("/user/gpg_keys/#{gpg_key.id}") + delete api(path) expect(response).to have_gitlab_http_status(:unauthorized) end it 'returns a 404 for invalid ID' do - delete api('/users/gpg_keys/ASDF', admin) + delete api('/users/gpg_keys/ASDF', admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end end describe "GET /user/emails" do + let(:path) { '/user/emails' } + context "when unauthenticated" do it "returns authentication error" do - get api("/user/emails") + get api(path) expect(response).to have_gitlab_http_status(:unauthorized) end end @@ -3160,7 +3298,7 @@ RSpec.describe API::Users, feature_category: :user_profile do it "returns array of emails" do user.emails << email - get api("/user/emails", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -3170,7 +3308,6 @@ RSpec.describe API::Users, feature_category: :user_profile do end context "scopes" do - let(:path) { "/user/emails" } let(:api_call) { method(:api) } include_examples 'allows the "read_user" scope' @@ -3179,10 +3316,12 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe "GET /user/emails/:email_id" do + let(:path) { "/user/emails/#{email.id}" } + it "returns single email" do user.emails << email - get api("/user/emails/#{email.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response["email"]).to eq(email.email) end @@ -3197,19 +3336,18 @@ RSpec.describe API::Users, feature_category: :user_profile do user.emails << email admin - get api("/user/emails/#{email.id}", admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Email Not Found') end it "returns 404 for invalid ID" do - get api("/users/emails/ASDF", admin) + get api("/users/emails/ASDF", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end context "scopes" do - let(:path) { "/user/emails/#{email.id}" } let(:api_call) { method(:api) } include_examples 'allows the "read_user" scope' @@ -3217,21 +3355,23 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe "POST /user/emails" do + let(:path) { '/user/emails' } + it "creates email" do email_attrs = attributes_for :email expect do - post api("/user/emails", user), params: email_attrs + post api(path, user), params: email_attrs end.to change { user.emails.count }.by(1) expect(response).to have_gitlab_http_status(:created) end it "returns a 401 error if unauthorized" do - post api("/user/emails"), params: { email: 'some email' } + post api(path), params: { email: 'some email' } expect(response).to have_gitlab_http_status(:unauthorized) end it "does not create email with invalid email" do - post api("/user/emails", user), params: {} + post api(path, user), params: {} expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq('email is missing') @@ -3239,18 +3379,20 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe "DELETE /user/emails/:email_id" do + let(:path) { "/user/emails/#{email.id}" } + it "deletes existed email" do user.emails << email expect do - delete api("/user/emails/#{email.id}", user) + delete api(path, user) expect(response).to have_gitlab_http_status(:no_content) end.to change { user.emails.count }.by(-1) end it_behaves_like '412 response' do - let(:request) { api("/user/emails/#{email.id}", user) } + let(:request) { api(path, user) } end it "returns 404 if email ID not found" do @@ -3263,12 +3405,12 @@ RSpec.describe API::Users, feature_category: :user_profile do it "returns 401 error if unauthorized" do user.emails << email - delete api("/user/emails/#{email.id}") + delete api(path) expect(response).to have_gitlab_http_status(:unauthorized) end it "returns 400 for invalid ID" do - delete api("/user/emails/ASDF", admin) + delete api("/user/emails/ASDF", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) end @@ -3283,12 +3425,18 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe 'POST /users/:id/activate' do - subject(:activate) { post api("/users/#{user_id}/activate", api_user) } + subject(:activate) { post api(path, api_user, **params) } let(:user_id) { user.id } + let(:path) { "/users/#{user_id}/activate" } + + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { {} } + end context 'performed by a non-admin user' do let(:api_user) { user } + let(:params) { { admin_mode: false } } it 'is not authorized to perform the action' do activate @@ -3299,6 +3447,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'performed by an admin user' do let(:api_user) { admin } + let(:params) { { admin_mode: true } } context 'for a deactivated user' do let(:user_id) { deactivated_user.id } @@ -3351,7 +3500,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end context 'for a user that does not exist' do - let(:user_id) { 0 } + let(:user_id) { non_existing_record_id } before do activate @@ -3363,12 +3512,18 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe 'POST /users/:id/deactivate' do - subject(:deactivate) { post api("/users/#{user_id}/deactivate", api_user) } + subject(:deactivate) { post api(path, api_user, **params) } let(:user_id) { user.id } + let(:path) { "/users/#{user_id}/deactivate" } + + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { {} } + end context 'performed by a non-admin user' do let(:api_user) { user } + let(:params) { { admin_mode: false } } it 'is not authorized to perform the action' do deactivate @@ -3379,6 +3534,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'performed by an admin user' do let(:api_user) { admin } + let(:params) { { admin_mode: true } } context 'for an active user' do let(:activity) { {} } @@ -3402,7 +3558,7 @@ RSpec.describe API::Users, feature_category: :user_profile do deactivate expect(response).to have_gitlab_http_status(:forbidden) - expect(json_response['message']).to eq("403 Forbidden - The user you are trying to deactivate has been active in the past #{Gitlab::CurrentSettings.deactivate_dormant_users_period} days and cannot be deactivated") + expect(json_response['message']).to eq("The user you are trying to deactivate has been active in the past #{Gitlab::CurrentSettings.deactivate_dormant_users_period} days and cannot be deactivated") expect(user.reload.state).to eq('active') end end @@ -3426,7 +3582,7 @@ RSpec.describe API::Users, feature_category: :user_profile do deactivate expect(response).to have_gitlab_http_status(:forbidden) - expect(json_response['message']).to eq('403 Forbidden - A blocked user cannot be deactivated by the API') + expect(json_response['message']).to eq('Error occurred. A blocked user cannot be deactivated') expect(blocked_user.reload.state).to eq('blocked') end end @@ -3440,7 +3596,7 @@ RSpec.describe API::Users, feature_category: :user_profile do deactivate expect(response).to have_gitlab_http_status(:forbidden) - expect(json_response['message']).to eq('403 Forbidden - A blocked user cannot be deactivated by the API') + expect(json_response['message']).to eq('Error occurred. A blocked user cannot be deactivated') expect(user.reload.state).to eq('ldap_blocked') end end @@ -3452,12 +3608,12 @@ RSpec.describe API::Users, feature_category: :user_profile do deactivate expect(response).to have_gitlab_http_status(:forbidden) - expect(json_response['message']).to eq('403 Forbidden - An internal user cannot be deactivated by the API') + expect(json_response['message']).to eq('Internal users cannot be deactivated') end end context 'for a user that does not exist' do - let(:user_id) { 0 } + let(:user_id) { non_existing_record_id } before do deactivate @@ -3480,11 +3636,19 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe 'POST /users/:id/approve' do - subject(:approve) { post api("/users/#{user_id}/approve", api_user) } + subject(:approve) { post api(path, api_user, **params) } + + let(:path) { "/users/#{user_id}/approve" } + + it_behaves_like 'POST request permissions for admin mode' do + let(:user_id) { pending_user.id } + let(:params) { {} } + end context 'performed by a non-admin user' do let(:api_user) { user } let(:user_id) { pending_user.id } + let(:params) { { admin_mode: false } } it 'is not authorized to perform the action' do expect { approve }.not_to change { pending_user.reload.state } @@ -3495,6 +3659,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'performed by an admin user' do let(:api_user) { admin } + let(:params) { { admin_mode: true } } context 'for a deactivated user' do let(:user_id) { deactivated_user.id } @@ -3558,8 +3723,16 @@ RSpec.describe API::Users, feature_category: :user_profile do end end - describe 'POST /users/:id/reject', :aggregate_failures do - subject(:reject) { post api("/users/#{user_id}/reject", api_user) } + describe 'POST /users/:id/reject' do + subject(:reject) { post api(path, api_user, **params) } + + let(:path) { "/users/#{user_id}/reject" } + + it_behaves_like 'POST request permissions for admin mode' do + let(:user_id) { pending_user.id } + let(:params) { {} } + let(:success_status_code) { :success } + end shared_examples 'returns 409' do it 'returns 409' do @@ -3573,6 +3746,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'performed by a non-admin user' do let(:api_user) { user } let(:user_id) { pending_user.id } + let(:params) { { admin_mode: false } } it 'returns 403' do expect { reject }.not_to change { pending_user.reload.state } @@ -3583,6 +3757,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'performed by an admin user' do let(:api_user) { admin } + let(:params) { { admin_mode: true } } context 'for an pending approval user' do let(:user_id) { pending_user.id } @@ -3648,13 +3823,21 @@ RSpec.describe API::Users, feature_category: :user_profile do end end - describe 'POST /users/:id/block', :aggregate_failures do + describe 'POST /users/:id/block' do + subject(:block_user) { post api(path, api_user, **params) } + + let(:user_id) { user.id } + let(:path) { "/users/#{user_id}/block" } + + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { {} } + end + context 'when admin' do - subject(:block_user) { post api("/users/#{user_id}/block", admin) } + let(:api_user) { admin } + let(:params) { { admin_mode: true } } context 'with an existing user' do - let(:user_id) { user.id } - it 'blocks existing user' do block_user @@ -3730,21 +3913,34 @@ RSpec.describe API::Users, feature_category: :user_profile do end end - it 'is not available for non admin users' do - post api("/users/#{user.id}/block", user) + context 'performed by a non-admin user' do + let(:api_user) { user } + let(:params) { { admin_mode: false } } - expect(response).to have_gitlab_http_status(:forbidden) - expect(user.reload.state).to eq('active') + it 'returns 403' do + block_user + + expect(response).to have_gitlab_http_status(:forbidden) + expect(user.reload.state).to eq('active') + end end end - describe 'POST /users/:id/unblock', :aggregate_failures do + describe 'POST /users/:id/unblock' do + subject(:unblock_user) { post api(path, api_user, **params) } + + let(:path) { "/users/#{user_id}/unblock" } + let(:user_id) { user.id } + + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { {} } + end + context 'when admin' do - subject(:unblock_user) { post api("/users/#{user_id}/unblock", admin) } + let(:api_user) { admin } + let(:params) { { admin_mode: true } } context 'with an existing user' do - let(:user_id) { user.id } - it 'unblocks existing user' do unblock_user @@ -3817,20 +4013,34 @@ RSpec.describe API::Users, feature_category: :user_profile do end end - it 'is not available for non admin users' do - post api("/users/#{user.id}/unblock", user) - expect(response).to have_gitlab_http_status(:forbidden) - expect(user.reload.state).to eq('active') + context 'performed by a non-admin user' do + let(:api_user) { user } + let(:params) { { admin_mode: false } } + + it 'returns 403' do + unblock_user + + expect(response).to have_gitlab_http_status(:forbidden) + expect(user.reload.state).to eq('active') + end end end - describe 'POST /users/:id/ban', :aggregate_failures do + describe 'POST /users/:id/ban' do + subject(:ban_user) { post api(path, api_user, **params) } + + let(:path) { "/users/#{user_id}/ban" } + let(:user_id) { user.id } + + it_behaves_like 'POST request permissions for admin mode' do + let(:params) { {} } + end + context 'when admin' do - subject(:ban_user) { post api("/users/#{user_id}/ban", admin) } + let(:api_user) { admin } + let(:params) { { admin_mode: true } } context 'with an active user' do - let(:user_id) { user.id } - it 'bans an active user' do ban_user @@ -3898,17 +4108,32 @@ RSpec.describe API::Users, feature_category: :user_profile do end end - it 'is not available for non-admin users' do - post api("/users/#{user.id}/ban", user) + context 'performed by a non-admin user' do + let(:api_user) { user } + let(:params) { { admin_mode: false } } - expect(response).to have_gitlab_http_status(:forbidden) - expect(user.reload.state).to eq('active') + it 'returns 403' do + ban_user + + expect(response).to have_gitlab_http_status(:forbidden) + expect(user.reload.state).to eq('active') + end end end - describe 'POST /users/:id/unban', :aggregate_failures do + describe 'POST /users/:id/unban' do + subject(:unban_user) { post api(path, api_user, **params) } + + let(:path) { "/users/#{user_id}/unban" } + + it_behaves_like 'POST request permissions for admin mode' do + let(:user_id) { banned_user.id } + let(:params) { {} } + end + context 'when admin' do - subject(:unban_user) { post api("/users/#{user_id}/unban", admin) } + let(:api_user) { admin } + let(:params) { { admin_mode: true } } context 'with a banned user' do let(:user_id) { banned_user.id } @@ -3979,37 +4204,42 @@ RSpec.describe API::Users, feature_category: :user_profile do end end - it 'is not available for non admin users' do - post api("/users/#{banned_user.id}/unban", user) + context 'performed by a non-admin user' do + let(:api_user) { user } + let(:params) { { admin_mode: false } } + let(:user_id) { banned_user.id } - expect(response).to have_gitlab_http_status(:forbidden) - expect(user.reload.state).to eq('active') + it 'returns 403' do + unban_user + + expect(response).to have_gitlab_http_status(:forbidden) + expect(user.reload.state).to eq('active') + end end end describe "GET /users/:id/memberships" do + subject(:request) { get api(path, requesting_user, admin_mode: true) } + let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } let_it_be(:group) { create(:group) } let(:requesting_user) { create(:user) } + let(:path) { "/users/#{user.id}/memberships" } before_all do project.add_guest(user) group.add_guest(user) end - it "responses with 403" do - get api("/users/#{user.id}/memberships", requesting_user) - - expect(response).to have_gitlab_http_status(:forbidden) - end + it_behaves_like 'GET request permissions for admin mode' context 'requested by admin user' do let(:requesting_user) { create(:user, :admin) } it "responses successfully" do - get api("/users/#{user.id}/memberships", requesting_user) + request aggregate_failures 'expect successful response including groups and projects' do expect(response).to have_gitlab_http_status(:ok) @@ -4024,22 +4254,23 @@ RSpec.describe API::Users, feature_category: :user_profile do it 'does not submit N+1 DB queries' do # Avoid setup queries - get api("/users/#{user.id}/memberships", requesting_user) + request + expect(response).to have_gitlab_http_status(:ok) control = ActiveRecord::QueryRecorder.new do - get api("/users/#{user.id}/memberships", requesting_user) + request end create_list(:project, 5).map { |project| project.add_guest(user) } expect do - get api("/users/#{user.id}/memberships", requesting_user) + request end.not_to exceed_query_limit(control) end context 'with type filter' do it "only returns project memberships" do - get api("/users/#{user.id}/memberships?type=Project", requesting_user) + get api("/users/#{user.id}/memberships?type=Project", requesting_user, admin_mode: true) aggregate_failures do expect(json_response).to contain_exactly(a_hash_including('source_type' => 'Project')) @@ -4048,7 +4279,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "only returns group memberships" do - get api("/users/#{user.id}/memberships?type=Namespace", requesting_user) + get api("/users/#{user.id}/memberships?type=Namespace", requesting_user, admin_mode: true) aggregate_failures do expect(json_response).to contain_exactly(a_hash_including('source_type' => 'Namespace')) @@ -4057,7 +4288,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it "recognizes unsupported types" do - get api("/users/#{user.id}/memberships?type=foo", requesting_user) + get api("/users/#{user.id}/memberships?type=foo", requesting_user, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) end @@ -4068,10 +4299,13 @@ RSpec.describe API::Users, feature_category: :user_profile do context "user activities", :clean_gitlab_redis_shared_state do let_it_be(:old_active_user) { create(:user, last_activity_on: Time.utc(2000, 1, 1)) } let_it_be(:newly_active_user) { create(:user, last_activity_on: 2.days.ago.midday) } + let(:path) { '/user/activities' } + + it_behaves_like 'GET request permissions for admin mode' context 'last activity as normal user' do it 'has no permission' do - get api("/user/activities", user) + get api(path, user) expect(response).to have_gitlab_http_status(:forbidden) end @@ -4079,7 +4313,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'as admin' do it 'returns the activities from the last 6 months' do - get api("/user/activities", admin) + get api(path, admin, admin_mode: true) expect(response).to include_pagination_headers expect(json_response.size).to eq(1) @@ -4093,7 +4327,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'passing a :from parameter' do it 'returns the activities from the given date' do - get api("/user/activities?from=2000-1-1", admin) + get api("#{path}?from=2000-1-1", admin, admin_mode: true) expect(response).to include_pagination_headers expect(json_response.size).to eq(2) @@ -4113,6 +4347,7 @@ RSpec.describe API::Users, feature_category: :user_profile do let(:user_with_status) { user_status.user } let(:params) { {} } let(:request_user) { user } + let(:path) { '/user/status' } shared_examples '/user/status successful response' do context 'when request is successful' do @@ -4150,7 +4385,7 @@ RSpec.describe API::Users, feature_category: :user_profile do set_user_status expect(response).to have_gitlab_http_status(:success) - expect(user_with_status.status).to be_nil + expect(user_with_status.reset.status).to be_nil end end end @@ -4178,7 +4413,7 @@ RSpec.describe API::Users, feature_category: :user_profile do set_user_status expect(response).to have_gitlab_http_status(:success) - expect(user_with_status.status.clear_status_at).to be_nil + expect(user_with_status.reset.status.clear_status_at).to be_nil end end @@ -4194,13 +4429,11 @@ RSpec.describe API::Users, feature_category: :user_profile do end describe 'GET' do - let(:path) { '/user/status' } - it_behaves_like 'rendering user status' end describe 'PUT' do - subject(:set_user_status) { put api('/user/status', request_user), params: params } + subject(:set_user_status) { put api(path, request_user), params: params } include_examples '/user/status successful response' @@ -4217,7 +4450,7 @@ RSpec.describe API::Users, feature_category: :user_profile do set_user_status expect(response).to have_gitlab_http_status(:success) - expect(user_with_status.status).to be_nil + expect(user_with_status.reset.status).to be_nil end end @@ -4229,13 +4462,13 @@ RSpec.describe API::Users, feature_category: :user_profile do set_user_status expect(response).to have_gitlab_http_status(:success) - expect(user_with_status.status.clear_status_at).to be_nil + expect(user_with_status.reset.status.clear_status_at).to be_nil end end end describe 'PATCH' do - subject(:set_user_status) { patch api('/user/status', request_user), params: params } + subject(:set_user_status) { patch api(path, request_user), params: params } include_examples '/user/status successful response' @@ -4274,57 +4507,41 @@ RSpec.describe API::Users, feature_category: :user_profile do let(:name) { 'new pat' } let(:expires_at) { 3.days.from_now.to_date.to_s } let(:scopes) { %w(api read_user) } + let(:path) { "/users/#{user.id}/personal_access_tokens" } + let(:params) { { name: name, scopes: scopes, expires_at: expires_at } } + + it_behaves_like 'POST request permissions for admin mode' it 'returns error if required attributes are missing' do - post api("/users/#{user.id}/personal_access_tokens", admin) + post api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq('name is missing, scopes is missing, scopes does not have a valid value') end it 'returns a 404 error if user not found' do - post api("/users/#{non_existing_record_id}/personal_access_tokens", admin), - params: { - name: name, - scopes: scopes, - expires_at: expires_at - } + post api("/users/#{non_existing_record_id}/personal_access_tokens", admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') end it 'returns a 401 error when not authenticated' do - post api("/users/#{user.id}/personal_access_tokens"), - params: { - name: name, - scopes: scopes, - expires_at: expires_at - } + post api(path), params: params expect(response).to have_gitlab_http_status(:unauthorized) expect(json_response['message']).to eq('401 Unauthorized') end it 'returns a 403 error when authenticated as normal user' do - post api("/users/#{user.id}/personal_access_tokens", user), - params: { - name: name, - scopes: scopes, - expires_at: expires_at - } + post api(path, user), params: params expect(response).to have_gitlab_http_status(:forbidden) expect(json_response['message']).to eq('403 Forbidden') end it 'creates a personal access token when authenticated as admin' do - post api("/users/#{user.id}/personal_access_tokens", admin), - params: { - name: name, - expires_at: expires_at, - scopes: scopes - } + post api(path, admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:created) expect(json_response['name']).to eq(name) @@ -4338,7 +4555,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end context 'when an error is thrown by the model' do - let!(:admin_personal_access_token) { create(:personal_access_token, user: admin) } + let!(:admin_personal_access_token) { create(:personal_access_token, :admin_mode, user: admin) } let(:error_message) { 'error message' } before do @@ -4351,12 +4568,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'returns the error' do - post api("/users/#{user.id}/personal_access_tokens", personal_access_token: admin_personal_access_token), - params: { - name: name, - expires_at: expires_at, - scopes: scopes - } + post api(path, personal_access_token: admin_personal_access_token), params: params expect(response).to have_gitlab_http_status(:unprocessable_entity) expect(json_response['message']).to eq(error_message) @@ -4370,9 +4582,12 @@ RSpec.describe API::Users, feature_category: :user_profile do let_it_be(:expired_personal_access_token) { create(:personal_access_token, :expired, user: user) } let_it_be(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) } let_it_be(:revoked_impersonation_token) { create(:personal_access_token, :impersonation, :revoked, user: user) } + let(:path) { "/users/#{user.id}/impersonation_tokens" } + + it_behaves_like 'GET request permissions for admin mode' it 'returns a 404 error if user not found' do - get api("/users/#{non_existing_record_id}/impersonation_tokens", admin) + get api("/users/#{non_existing_record_id}/impersonation_tokens", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') @@ -4386,7 +4601,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'returns an array of all impersonated tokens' do - get api("/users/#{user.id}/impersonation_tokens", admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -4395,7 +4610,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'returns an array of active impersonation tokens if state active' do - get api("/users/#{user.id}/impersonation_tokens?state=active", admin) + get api("#{path}?state=active", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -4405,7 +4620,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'returns an array of inactive personal access tokens if active is set to false' do - get api("/users/#{user.id}/impersonation_tokens?state=inactive", admin) + get api("#{path}?state=inactive", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array @@ -4419,16 +4634,20 @@ RSpec.describe API::Users, feature_category: :user_profile do let(:expires_at) { '2016-12-28' } let(:scopes) { %w(api read_user) } let(:impersonation) { true } + let(:path) { "/users/#{user.id}/impersonation_tokens" } + let(:params) { { name: name, expires_at: expires_at, scopes: scopes, impersonation: impersonation } } + + it_behaves_like 'POST request permissions for admin mode' it 'returns validation error if impersonation token misses some attributes' do - post api("/users/#{user.id}/impersonation_tokens", admin) + post api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq('name is missing') end it 'returns a 404 error if user not found' do - post api("/users/#{non_existing_record_id}/impersonation_tokens", admin), + post api("/users/#{non_existing_record_id}/impersonation_tokens", admin, admin_mode: true), params: { name: name, expires_at: expires_at @@ -4439,7 +4658,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'returns a 403 error when authenticated as normal user' do - post api("/users/#{user.id}/impersonation_tokens", user), + post api(path, user), params: { name: name, expires_at: expires_at @@ -4450,13 +4669,7 @@ RSpec.describe API::Users, feature_category: :user_profile do end it 'creates a impersonation token' do - post api("/users/#{user.id}/impersonation_tokens", admin), - params: { - name: name, - expires_at: expires_at, - scopes: scopes, - impersonation: impersonation - } + post api(path, admin, admin_mode: true), params: params expect(response).to have_gitlab_http_status(:created) expect(json_response['name']).to eq(name) @@ -4474,37 +4687,40 @@ RSpec.describe API::Users, feature_category: :user_profile do describe 'GET /users/:user_id/impersonation_tokens/:impersonation_token_id' do let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } let_it_be(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) } + let(:path) { "/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}" } + + it_behaves_like 'GET request permissions for admin mode' it 'returns 404 error if user not found' do - get api("/users/#{non_existing_record_id}/impersonation_tokens/1", admin) + get api("/users/#{non_existing_record_id}/impersonation_tokens/1", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') end it 'returns a 404 error if impersonation token not found' do - get api("/users/#{user.id}/impersonation_tokens/#{non_existing_record_id}", admin) + get api("/users/#{user.id}/impersonation_tokens/#{non_existing_record_id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Impersonation Token Not Found') end it 'returns a 404 error if token is not impersonation token' do - get api("/users/#{user.id}/impersonation_tokens/#{personal_access_token.id}", admin) + get api("/users/#{user.id}/impersonation_tokens/#{personal_access_token.id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Impersonation Token Not Found') end it 'returns a 403 error when authenticated as normal user' do - get api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", user) + get api(path, user) expect(response).to have_gitlab_http_status(:forbidden) expect(json_response['message']).to eq('403 Forbidden') end it 'returns an impersonation token' do - get api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response['token']).not_to be_present @@ -4515,41 +4731,44 @@ RSpec.describe API::Users, feature_category: :user_profile do describe 'DELETE /users/:user_id/impersonation_tokens/:impersonation_token_id' do let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } let_it_be(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) } + let(:path) { "/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}" } + + it_behaves_like 'DELETE request permissions for admin mode' it 'returns a 404 error if user not found' do - delete api("/users/#{non_existing_record_id}/impersonation_tokens/1", admin) + delete api("/users/#{non_existing_record_id}/impersonation_tokens/1", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 User Not Found') end it 'returns a 404 error if impersonation token not found' do - delete api("/users/#{user.id}/impersonation_tokens/#{non_existing_record_id}", admin) + delete api("/users/#{user.id}/impersonation_tokens/#{non_existing_record_id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Impersonation Token Not Found') end it 'returns a 404 error if token is not impersonation token' do - delete api("/users/#{user.id}/impersonation_tokens/#{personal_access_token.id}", admin) + delete api("/users/#{user.id}/impersonation_tokens/#{personal_access_token.id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) expect(json_response['message']).to eq('404 Impersonation Token Not Found') end it 'returns a 403 error when authenticated as normal user' do - delete api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", user) + delete api(path, user) expect(response).to have_gitlab_http_status(:forbidden) expect(json_response['message']).to eq('403 Forbidden') end it_behaves_like '412 response' do - let(:request) { api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin) } + let(:request) { api(path, admin, admin_mode: true) } end it 'revokes a impersonation token' do - delete api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin) + delete api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:no_content) expect(impersonation_token.revoked).to be_falsey @@ -4560,6 +4779,7 @@ RSpec.describe API::Users, feature_category: :user_profile do describe 'GET /users/:id/associations_count' do let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, :public, group: group) } + let(:path) { "/users/#{user.id}/associations_count" } let(:associations) do { groups_count: 1, @@ -4576,9 +4796,11 @@ RSpec.describe API::Users, feature_category: :user_profile do create_list(:issue, 2, project: project, author: user) end + it_behaves_like 'GET request permissions for admin mode' + context 'as an unauthorized user' do it 'returns 401 unauthorized' do - get api("/users/#{user.id}/associations_count", nil) + get api(path, nil) expect(response).to have_gitlab_http_status(:unauthorized) end @@ -4595,7 +4817,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'with the current user id' do it 'returns valid JSON response' do - get api("/users/#{user.id}/associations_count", user) + get api(path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_a Hash @@ -4607,7 +4829,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'as an admin user' do context 'with invalid user id' do it 'returns 404 User Not Found' do - get api("/users/#{non_existing_record_id}/associations_count", admin) + get api("/users/#{non_existing_record_id}/associations_count", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:not_found) end @@ -4615,7 +4837,7 @@ RSpec.describe API::Users, feature_category: :user_profile do context 'with valid user id' do it 'returns valid JSON response' do - get api("/users/#{user.id}/associations_count", admin) + get api(path, admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_a Hash @@ -4629,4 +4851,169 @@ RSpec.describe API::Users, feature_category: :user_profile do let(:attributable) { user } let(:other_attributable) { admin } end + + describe 'POST /user/runners', feature_category: :runner_fleet do + subject(:request) { post api(path, current_user, **post_args), params: runner_attrs } + + let_it_be(:group_owner) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, namespace: group) } + + let(:post_args) { { admin_mode: true } } + let(:runner_attrs) { { runner_type: 'instance_type' } } + let(:path) { '/user/runners' } + + before do + group.add_owner(group_owner) + end + + shared_context 'returns forbidden when user does not have sufficient permissions' do + let(:current_user) { admin } + let(:post_args) { { admin_mode: false } } + + it 'does not create a runner' do + expect do + request + + expect(response).to have_gitlab_http_status(:forbidden) + end.not_to change { Ci::Runner.count } + end + end + + shared_examples 'creates a runner' do + it 'creates a runner' do + expect do + request + + expect(response).to have_gitlab_http_status(:created) + end.to change { Ci::Runner.count }.by(1) + end + end + + shared_examples 'fails to create runner with :bad_request' do + it 'does not create runner' do + expect do + request + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to include(expected_error) + end.not_to change { Ci::Runner.count } + end + end + + context 'when runner_type is :instance_type' do + let(:runner_attrs) { { runner_type: 'instance_type' } } + + context 'when user has sufficient permissions' do + let(:current_user) { admin } + + it_behaves_like 'creates a runner' + end + + it_behaves_like 'returns forbidden when user does not have sufficient permissions' + + context 'when model validation fails' do + let(:runner_attrs) { { runner_type: 'instance_type', run_untagged: false, tag_list: [] } } + let(:current_user) { admin } + + it_behaves_like 'fails to create runner with :bad_request' do + let(:expected_error) { 'Tags list can not be empty' } + end + end + end + + context 'when runner_type is :group_type' do + let(:post_args) { {} } + + context 'when group_id is specified' do + let(:runner_attrs) { { runner_type: 'group_type', group_id: group.id } } + + context 'when user has sufficient permissions' do + let(:current_user) { group_owner } + + it_behaves_like 'creates a runner' + end + + it_behaves_like 'returns forbidden when user does not have sufficient permissions' + end + + context 'when group_id is not specified' do + let(:runner_attrs) { { runner_type: 'group_type' } } + let(:current_user) { group_owner } + + it 'fails to create runner with :bad_request' do + expect do + request + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to include('group_id is missing') + end.not_to change { Ci::Runner.count } + end + end + end + + context 'when runner_type is :project_type' do + let(:post_args) { {} } + + context 'when project_id is specified' do + let(:runner_attrs) { { runner_type: 'project_type', project_id: project.id } } + + context 'when user has sufficient permissions' do + let(:current_user) { group_owner } + + it_behaves_like 'creates a runner' + end + + it_behaves_like 'returns forbidden when user does not have sufficient permissions' + end + + context 'when project_id is not specified' do + let(:runner_attrs) { { runner_type: 'project_type' } } + let(:current_user) { group_owner } + + it 'fails to create runner with :bad_request' do + expect do + request + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to include('project_id is missing') + end.not_to change { Ci::Runner.count } + end + end + end + + context 'with missing runner_type' do + let(:runner_attrs) { {} } + let(:current_user) { admin } + + it 'fails to create runner with :bad_request' do + expect do + request + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('runner_type is missing, runner_type does not have a valid value') + end.not_to change { Ci::Runner.count } + end + end + + context 'with unknown runner_type' do + let(:runner_attrs) { { runner_type: 'unknown' } } + let(:current_user) { admin } + + it 'fails to create runner with :bad_request' do + expect do + request + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('runner_type does not have a valid value') + end.not_to change { Ci::Runner.count } + end + end + + it 'returns a 401 error if unauthorized' do + post api(path), params: runner_attrs + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end end diff --git a/spec/requests/api/v3/github_spec.rb b/spec/requests/api/v3/github_spec.rb index 0b8fac5c55c..b6fccd9b7cb 100644 --- a/spec/requests/api/v3/github_spec.rb +++ b/spec/requests/api/v3/github_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::V3::Github, feature_category: :integrations do +RSpec.describe API::V3::Github, :aggregate_failures, feature_category: :integrations do let_it_be(:user) { create(:user) } let_it_be(:unauthorized_user) { create(:user) } let_it_be(:admin) { create(:user, :admin) } @@ -13,6 +13,13 @@ RSpec.describe API::V3::Github, feature_category: :integrations do end describe 'GET /orgs/:namespace/repos' do + it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do + subject do + group = create(:group) + jira_get v3_api("/orgs/#{group.path}/repos", user) + end + end + it 'returns an empty array' do group = create(:group) @@ -32,6 +39,10 @@ RSpec.describe API::V3::Github, feature_category: :integrations do end describe 'GET /user/repos' do + it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do + subject { jira_get v3_api('/user/repos', user) } + end + it 'returns an empty array' do jira_get v3_api('/user/repos', user) @@ -117,6 +128,10 @@ RSpec.describe API::V3::Github, feature_category: :integrations do describe 'GET /users/:username' do let!(:user1) { create(:user, username: 'jane.porter') } + it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do + subject { jira_get v3_api("/users/#{user.username}", user) } + end + context 'user exists' do it 'responds with the expected user' do jira_get v3_api("/users/#{user.username}", user) @@ -155,6 +170,10 @@ RSpec.describe API::V3::Github, feature_category: :integrations do let(:project) { create(:project, :empty_repo, path: 'project.with.dot', group: group) } let(:events_path) { "/repos/#{group.path}/#{project.path}/events" } + it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do + subject { jira_get v3_api(events_path, user) } + end + context 'if there are no merge requests' do it 'returns an empty array' do jira_get v3_api(events_path, user) @@ -232,6 +251,10 @@ RSpec.describe API::V3::Github, feature_category: :integrations do describe 'GET /-/jira/pulls' do let(:route) { '/repos/-/jira/pulls' } + it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do + subject { perform_request } + end + it 'returns an array of merge requests with github format' do perform_request @@ -258,6 +281,10 @@ RSpec.describe API::V3::Github, feature_category: :integrations do describe 'GET /repos/:namespace/:project/pulls' do let(:route) { "/repos/#{project.namespace.path}/#{project.path}/pulls" } + it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do + subject { perform_request } + end + it 'returns an array of merge requests for the proper project in github format' do perform_request @@ -279,6 +306,10 @@ RSpec.describe API::V3::Github, feature_category: :integrations do end describe 'GET /repos/:namespace/:project/pulls/:id' do + it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do + subject { jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", user) } + end + context 'when user has access to the merge requests' do it 'returns the requested merge request in github format' do jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", user) @@ -300,7 +331,7 @@ RSpec.describe API::V3::Github, feature_category: :integrations do context 'when instance admin' do it 'returns the requested merge request in github format' do - jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", admin) + jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", admin, admin_mode: true) expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('entities/github/pull_request') @@ -312,8 +343,8 @@ RSpec.describe API::V3::Github, feature_category: :integrations do describe 'GET /users/:namespace/repos' do let(:group) { create(:group, name: 'foo') } - def expect_project_under_namespace(projects, namespace, user) - jira_get v3_api("/users/#{namespace.path}/repos", user) + def expect_project_under_namespace(projects, namespace, user, admin_mode = false) + jira_get v3_api("/users/#{namespace.path}/repos", user, admin_mode: admin_mode) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers @@ -331,6 +362,10 @@ RSpec.describe API::V3::Github, feature_category: :integrations do expect(json_response.size).to eq(projects.size) end + it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do + subject { jira_get v3_api("/users/#{user.namespace.path}/repos", user) } + end + context 'group namespace' do let(:project) { create(:project, group: group) } let!(:project2) { create(:project, :public, group: group) } @@ -343,7 +378,7 @@ RSpec.describe API::V3::Github, feature_category: :integrations do let(:user) { create(:user, :admin) } it 'returns an array of projects belonging to group' do - expect_project_under_namespace([project, project2], group, user) + expect_project_under_namespace([project, project2], group, user, true) end context 'with a private group' do @@ -351,7 +386,7 @@ RSpec.describe API::V3::Github, feature_category: :integrations do let!(:project2) { create(:project, :private, group: group) } it 'returns an array of projects belonging to group' do - expect_project_under_namespace([project, project2], group, user) + expect_project_under_namespace([project, project2], group, user, true) end end end @@ -423,6 +458,10 @@ RSpec.describe API::V3::Github, feature_category: :integrations do describe 'GET /repos/:namespace/:project/branches' do context 'authenticated' do + it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do + subject { jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user) } + end + context 'updating project feature usage' do it 'counts Jira Cloud integration as enabled' do user_agent = 'Jira DVCS Connector Vertigo/4.42.0' @@ -473,7 +512,7 @@ RSpec.describe API::V3::Github, feature_category: :integrations do expect(response).to have_gitlab_http_status(:ok) end - context 'when the project has no repository', :aggregate_failures do + context 'when the project has no repository' do let_it_be(:project) { create(:project, creator: user) } it 'returns an empty collection response' do @@ -516,7 +555,11 @@ RSpec.describe API::V3::Github, feature_category: :integrations do end context 'authenticated' do - it 'returns commit with github format', :aggregate_failures do + it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do + subject { call_api } + end + + it 'returns commit with github format' do call_api expect(response).to have_gitlab_http_status(:ok) @@ -552,7 +595,7 @@ RSpec.describe API::V3::Github, feature_category: :integrations do .and_call_original end - it 'handles the error, logs it, and returns empty diff files', :aggregate_failures do + it 'handles the error, logs it, and returns empty diff files' do allow(Gitlab::GitalyClient).to receive(:call) .with(*commit_diff_args) .and_raise(GRPC::DeadlineExceeded) @@ -567,7 +610,7 @@ RSpec.describe API::V3::Github, feature_category: :integrations do expect(response_diff_files(response)).to be_blank end - it 'only calls Gitaly once for all attempts within a period of time', :aggregate_failures do + it 'only calls Gitaly once for all attempts within a period of time' do expect(Gitlab::GitalyClient).to receive(:call) .with(*commit_diff_args) .once # <- once @@ -581,7 +624,7 @@ RSpec.describe API::V3::Github, feature_category: :integrations do end end - it 'calls Gitaly again after a period of time', :aggregate_failures do + it 'calls Gitaly again after a period of time' do expect(Gitlab::GitalyClient).to receive(:call) .with(*commit_diff_args) .twice # <- twice @@ -648,13 +691,14 @@ RSpec.describe API::V3::Github, feature_category: :integrations do get path, headers: { 'User-Agent' => user_agent } end - def v3_api(path, user = nil, personal_access_token: nil, oauth_access_token: nil) + def v3_api(path, user = nil, personal_access_token: nil, oauth_access_token: nil, admin_mode: false) api( path, user, version: 'v3', personal_access_token: personal_access_token, - oauth_access_token: oauth_access_token + oauth_access_token: oauth_access_token, + admin_mode: admin_mode ) end end |