diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-20 12:26:25 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-20 12:26:25 +0000 |
commit | a09983ae35713f5a2bbb100981116d31ce99826e (patch) | |
tree | 2ee2af7bd104d57086db360a7e6d8c9d5d43667a /spec/requests/api | |
parent | 18c5ab32b738c0b6ecb4d0df3994000482f34bd8 (diff) | |
download | gitlab-ce-a09983ae35713f5a2bbb100981116d31ce99826e.tar.gz |
Add latest changes from gitlab-org/gitlab@13-2-stable-ee
Diffstat (limited to 'spec/requests/api')
219 files changed, 7358 insertions, 964 deletions
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb index 52bc81cff18..223d740a004 100644 --- a/spec/requests/api/access_requests_spec.rb +++ b/spec/requests/api/access_requests_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::AccessRequests do +RSpec.describe API::AccessRequests 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/ci/variables_spec.rb b/spec/requests/api/admin/ci/variables_spec.rb index 185fde17e1b..812ee93ad21 100644 --- a/spec/requests/api/admin/ci/variables_spec.rb +++ b/spec/requests/api/admin/ci/variables_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe ::API::Admin::Ci::Variables do +RSpec.describe ::API::Admin::Ci::Variables do let_it_be(:admin) { create(:admin) } let_it_be(:user) { create(:user) } diff --git a/spec/requests/api/admin/instance_clusters_spec.rb b/spec/requests/api/admin/instance_clusters_spec.rb new file mode 100644 index 00000000000..b68541b5d92 --- /dev/null +++ b/spec/requests/api/admin/instance_clusters_spec.rb @@ -0,0 +1,461 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::API::Admin::InstanceClusters 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 + create(:cluster, :project, :provided_by_gcp, + user: admin_user, + projects: [project]) + end + let(:project_cluster_id) { project_cluster.id } + + describe "GET /admin/clusters" do + let_it_be(:clusters) do + create_list(:cluster, 3, :provided_by_gcp, :instance, :production_environment) + end + + 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 + 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) + end + + it 'includes pagination headers' do + expect(response).to include_pagination_headers + end + + it 'only returns the instance clusters' 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) + end + end + end + + describe "GET /admin/clusters/:cluster_id" do + let_it_be(:platform_kubernetes) do + create(:cluster_platform_kubernetes, :configured) + end + + let_it_be(:cluster) do + create(:cluster, :instance, :provided_by_gcp, :with_domain, + platform_kubernetes: platform_kubernetes, + user: admin_user) + end + + let(:cluster_id) { cluster.id } + + context "when authenticated as admin" do + before do + get api("/admin/clusters/#{cluster_id}", admin_user) + end + + context "when no cluster associated to the ID" do + let(:cluster_id) { 1337 } + + it 'returns 404' do + expect(response).to have_gitlab_http_status(:not_found) + end + 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 + expect(json_response['provider_type']).to eq('gcp') + expect(json_response['platform_type']).to eq('kubernetes') + expect(json_response['environment_scope']).to eq('*') + expect(json_response['cluster_type']).to eq('instance_type') + expect(json_response['domain']).to eq('example.com') + end + + it 'returns kubernetes platform information' 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 + 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 + gcp_provider = json_response['provider_gcp'] + + expect(gcp_provider['cluster_id']).to eq(cluster.id) + expect(gcp_provider['status_name']).to eq('created') + expect(gcp_provider['gcp_project_id']).to eq('test-gcp-project') + expect(gcp_provider['zone']).to eq('us-central1-a') + expect(gcp_provider['machine_type']).to eq('n1-standard-2') + expect(gcp_provider['num_nodes']).to eq(3) + expect(gcp_provider['endpoint']).to eq('111.111.111.111') + end + + context 'when cluster has no provider' do + let(:cluster) do + create(:cluster, :instance, :provided_by_user, :production_environment) + end + + it 'does not include GCP provider info' do + expect(json_response['provider_gcp']).not_to be_present + end + end + + 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) + 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 + + describe "POST /admin/clusters/add" do + let(:api_url) { 'https://example.com' } + let(:authorization_type) { 'rbac' } + let(:clusterable) { Clusters::Instance.new } + + let(:platform_kubernetes_attributes) do + { + api_url: api_url, + token: 'sample-token', + authorization_type: authorization_type + } + end + + let(:cluster_params) do + { + name: 'test-instance-cluster', + domain: 'domain.example.com', + managed: false, + platform_kubernetes_attributes: platform_kubernetes_attributes, + clusterable: clusterable + } + end + + let(:multiple_cluster_params) do + { + name: 'multiple-instance-cluster', + environment_scope: 'staging/*', + platform_kubernetes_attributes: platform_kubernetes_attributes + } + end + + let(:invalid_cluster_params) do + { + environment_scope: 'production/*', + domain: 'domain.example.com', + platform_kubernetes_attributes: platform_kubernetes_attributes + } + end + + context 'authorized user' do + before do + post api('/admin/clusters/add', admin_user), 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 + expect(cluster_result).to be_user + expect(cluster_result).to be_kubernetes + expect(cluster_result.clusterable).to be_a Clusters::Instance + expect(cluster_result.cluster_type).to eq('instance_type') + expect(cluster_result.name).to eq('test-instance-cluster') + expect(cluster_result.domain).to eq('domain.example.com') + expect(cluster_result.environment_scope).to eq('*') + expect(cluster_result.enabled).to eq(true) + expect(platform_kubernetes.authorization_type).to eq('rbac') + expect(cluster_result.managed).to be_falsy + expect(platform_kubernetes.api_url).to eq("https://example.com") + expect(platform_kubernetes.token).to eq('sample-token') + end + + context 'when user does not indicate authorization type' do + let(:platform_kubernetes_attributes) do + { + api_url: api_url, + token: 'sample-token' + } + end + + it 'defaults to RBAC' do + cluster_result = Clusters::Cluster.find(json_response['id']) + + expect(cluster_result.platform_kubernetes.rbac?).to be_truthy + end + end + + context 'when user sets authorization type as ABAC' do + let(:authorization_type) { 'abac' } + + it 'creates an ABAC cluster' do + cluster_result = Clusters::Cluster.find(json_response['id']) + + expect(cluster_result.platform.abac?).to be_truthy + end + end + + 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 + + expect(Clusters::Instance.new.clusters.count).to eq(2) + end + end + end + + 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 + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eql('name is missing') + end + end + + context 'with a malformed api url' do + let(:api_url) { 'invalid_api_url' } + + it 'responds with 400' do + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns validation errors' do + expect(json_response['message']['platform_kubernetes.api_url'].first).to be_present + end + 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 + let(:api_url) { 'https://example.com' } + + let(:update_params) do + { + domain: domain, + platform_kubernetes_attributes: platform_kubernetes_attributes + } + end + + let(:domain) { 'new-domain.com' } + let(:platform_kubernetes_attributes) { {} } + + let_it_be(:cluster) do + create(:cluster, :instance, :provided_by_gcp, domain: 'old-domain.com') + end + + context 'authorized user' do + before do + put api("/admin/clusters/#{cluster.id}", admin_user), 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 + expect(cluster.domain).to eq('new-domain.com') + end + end + + context 'with invalid params' do + let(:domain) { 'invalid domain' } + + it 'responds with 400' do + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'does not update cluster attributes' do + expect(cluster.domain).to eq('old-domain.com') + end + + it 'returns validation errors' do + expect(json_response['message']['domain'].first).to match('contains invalid characters (valid characters: [a-z0-9\\-])') + end + end + + context 'with a GCP cluster' do + context 'when user tries to change GCP specific fields' do + let(:platform_kubernetes_attributes) do + { + api_url: 'https://new-api-url.com', + token: 'new-sample-token' + } + end + + it 'responds with 400' do + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns validation error' do + expect(json_response['message']['platform_kubernetes.base'].first).to eq(_('Cannot modify managed Kubernetes cluster')) + end + end + + context 'when user tries to change domain' do + let(:domain) { 'new-domain.com' } + + it 'responds with 200' do + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'with an user cluster' do + let(:api_url) { 'https://new-api-url.com' } + + let(:cluster) do + create(:cluster, :instance, :provided_by_user, :production_environment) + end + + let(:platform_kubernetes_attributes) do + { + api_url: api_url, + token: 'new-sample-token' + } + end + + let(:update_params) do + { + name: 'new-name', + platform_kubernetes_attributes: platform_kubernetes_attributes + } + end + + it 'responds with 200' do + expect(response).to have_gitlab_http_status(:ok) + end + + it 'updates platform kubernetes attributes' do + platform_kubernetes = cluster.platform_kubernetes + + expect(cluster.name).to eq('new-name') + expect(platform_kubernetes.api_url).to eq('https://new-api-url.com') + expect(platform_kubernetes.token).to eq('new-sample-token') + end + end + + context 'with a cluster that does not exist' do + let(:cluster_id) { 1337 } + + it 'returns 404' do + put api("/admin/clusters/#{cluster_id}", admin_user), 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 + 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 + let(:cluster_params) { { cluster_id: cluster.id } } + + let_it_be(:cluster) do + create(:cluster, :instance, :provided_by_gcp) + 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) + end + + it 'deletes the cluster' do + expect(Clusters::Cluster.exists?(id: cluster.id)).to be_falsy + end + + context 'with a cluster that does not exist' do + let(:cluster_id) { 1337 } + + it 'returns 404' do + delete api("/admin/clusters/#{cluster_id}", admin_user) + 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) + 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/sidekiq_spec.rb b/spec/requests/api/admin/sidekiq_spec.rb index 303b62f4436..3c488816bed 100644 --- a/spec/requests/api/admin/sidekiq_spec.rb +++ b/spec/requests/api/admin/sidekiq_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Admin::Sidekiq, :clean_gitlab_redis_queues do +RSpec.describe API::Admin::Sidekiq, :clean_gitlab_redis_queues do let_it_be(:admin) { create(:admin) } describe 'DELETE /admin/sidekiq/queues/:queue_name' do 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 7175076e56d..4b477f829a7 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' -describe API::APIGuard::AdminModeMiddleware, :do_not_mock_admin_mode, :request_store do +RSpec.describe API::APIGuard::AdminModeMiddleware, :do_not_mock_admin_mode, :request_store do let(:user) { create(:admin) } it 'is loaded' do diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb index 201c0d1796c..bd0426601db 100644 --- a/spec/requests/api/api_spec.rb +++ b/spec/requests/api/api_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::API do +RSpec.describe API::API do include GroupAPIHelpers describe 'Record user last activity in after hook' do @@ -36,6 +36,14 @@ describe API::API do expect(response).to have_gitlab_http_status(:ok) end + it 'does not authorize user for revoked token' do + revoked = create(:personal_access_token, :revoked, user: user, scopes: [:read_api]) + + get api('/groups', personal_access_token: revoked) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + it 'does not authorize user for post request' do params = attributes_for_group_api diff --git a/spec/requests/api/appearance_spec.rb b/spec/requests/api/appearance_spec.rb index f8c3db70d16..69176e18d2e 100644 --- a/spec/requests/api/appearance_spec.rb +++ b/spec/requests/api/appearance_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Appearance, 'Appearance' do +RSpec.describe API::Appearance, 'Appearance' do let_it_be(:user) { create(:user) } let_it_be(:admin) { create(:admin) } diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb index cd341ad134e..63fbf6e32dd 100644 --- a/spec/requests/api/applications_spec.rb +++ b/spec/requests/api/applications_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Applications, :api do +RSpec.describe API::Applications, :api do let(:admin_user) { create(:user, admin: true) } let(:user) { create(:user, admin: false) } let!(:application) { create(:application, name: 'another_application', redirect_uri: 'http://other_application.url', scopes: '') } @@ -74,14 +74,15 @@ describe API::Applications, :api do expect(json_response['error']).to eq('scopes is missing') end - it 'does not allow creating an application with confidential set to nil' do + it 'defaults to creating an application with confidential' do expect do post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: '', confidential: nil } - end.not_to change { Doorkeeper::Application.count } + end.to change { Doorkeeper::Application.count }.by(1) - expect(response).to have_gitlab_http_status(:bad_request) + expect(response).to have_gitlab_http_status(:created) expect(json_response).to be_a Hash - expect(json_response['message']['confidential'].first).to eq('is not included in the list') + expect(json_response['callback_url']).to eq('http://application.url') + expect(json_response['confidential']).to be true end end diff --git a/spec/requests/api/avatar_spec.rb b/spec/requests/api/avatar_spec.rb index 45e34b7894b..656a086e550 100644 --- a/spec/requests/api/avatar_spec.rb +++ b/spec/requests/api/avatar_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Avatar do +RSpec.describe API::Avatar do let(:gravatar_service) { double('GravatarService') } describe 'GET /avatar' do diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index 543fe970abd..1c825949ae8 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::AwardEmoji do +RSpec.describe API::AwardEmoji do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } 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 d7f9b7d010b..99d224cb8e9 100644 --- a/spec/requests/api/badges_spec.rb +++ b/spec/requests/api/badges_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Badges do +RSpec.describe API::Badges do let(:maintainer) { create(:user, username: 'maintainer_user') } let(:developer) { create(:user) } let(:access_requester) { create(:user) } diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb index d761b371821..f0d3afd0af7 100644 --- a/spec/requests/api/boards_spec.rb +++ b/spec/requests/api/boards_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Boards do +RSpec.describe API::Boards do let_it_be(:user) { create(:user) } let_it_be(:non_member) { create(:user) } let_it_be(:guest) { create(:user) } diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index f2dc5b1c045..46acd92803f 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Branches do +RSpec.describe API::Branches do let_it_be(:user) { create(:user) } let(:project) { create(:project, :repository, creator: user, path: 'my.project') } let(:guest) { create(:user).tap { |u| project.add_guest(u) } } @@ -17,6 +17,7 @@ describe API::Branches do before do project.add_maintainer(user) project.repository.add_branch(user, 'ends-with.txt', branch_sha) + stub_feature_flags(branch_list_keyset_pagination: false) end describe "GET /projects/:id/repository/branches" do @@ -29,16 +30,6 @@ describe API::Branches do end end - it 'returns the repository branches' do - get api(route, current_user), params: { per_page: 100 } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/branches') - expect(response).to include_pagination_headers - branch_names = json_response.map { |x| x['name'] } - expect(branch_names).to match_array(project.repository.branch_names) - end - def check_merge_status(json_response) merged, unmerged = json_response.partition { |branch| branch['merged'] } merged_branches = merged.map { |branch| branch['name'] } @@ -47,22 +38,107 @@ describe API::Branches do expect(project.repository.merged_branch_names(unmerged_branches)).to be_empty end - it 'determines only a limited number of merged branch names' do - expect(API::Entities::Branch).to receive(:represent).with(anything, has_up_to_merged_branch_names_count(2)).and_call_original + context 'with branch_list_keyset_pagination feature off' do + context 'with legacy pagination params' do + it 'returns the repository branches' do + get api(route, current_user), params: { per_page: 100 } - get api(route, current_user), params: { per_page: 2 } + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/branches') + expect(response).to include_pagination_headers + branch_names = json_response.map { |x| x['name'] } + expect(branch_names).to match_array(project.repository.branch_names) + end - expect(response).to have_gitlab_http_status(:ok) + it 'determines only a limited number of merged branch names' do + expect(API::Entities::Branch).to receive(:represent).with(anything, has_up_to_merged_branch_names_count(2)).and_call_original + + get api(route, current_user), params: { per_page: 2 } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq 2 + + check_merge_status(json_response) + end - check_merge_status(json_response) + it 'merge status matches reality on paginated input' do + expected_first_branch_name = project.repository.branches_sorted_by('name')[20].name + + get api(route, current_user), params: { per_page: 20, page: 2 } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq 20 + expect(json_response.first['name']).to eq(expected_first_branch_name) + + check_merge_status(json_response) + end + end + + context 'with gitaly pagination params ' do + it 'merge status matches reality on paginated input' do + expected_first_branch_name = project.repository.branches_sorted_by('name').first.name + + get api(route, current_user), params: { per_page: 20, page_token: 'feature' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq 20 + expect(json_response.first['name']).to eq(expected_first_branch_name) + + check_merge_status(json_response) + end + end end - it 'merge status matches reality on paginated input' do - get api(route, current_user), params: { per_page: 20, page: 2 } + context 'with branch_list_keyset_pagination feature on' do + before do + stub_feature_flags(branch_list_keyset_pagination: true) + end - expect(response).to have_gitlab_http_status(:ok) + context 'with gitaly pagination params ' do + it 'returns the repository branches' do + get api(route, current_user), params: { per_page: 100 } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/branches') + branch_names = json_response.map { |x| x['name'] } + expect(branch_names).to match_array(project.repository.branch_names) + end + + it 'determines only a limited number of merged branch names' do + expect(API::Entities::Branch).to receive(:represent).with(anything, has_up_to_merged_branch_names_count(2)).and_call_original + + get api(route, current_user), params: { per_page: 2 } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq 2 + + check_merge_status(json_response) + end - check_merge_status(json_response) + it 'merge status matches reality on paginated input' do + expected_first_branch_name = project.repository.branches_sorted_by('name').drop_while { |b| b.name <= 'feature' }.first.name + + get api(route, current_user), params: { per_page: 20, page_token: 'feature' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq 20 + expect(json_response.first['name']).to eq(expected_first_branch_name) + + check_merge_status(json_response) + end + end + + context 'with legacy pagination params' do + it 'ignores legacy pagination params' do + expected_first_branch_name = project.repository.branches_sorted_by('name').first.name + get api(route, current_user), params: { per_page: 20, page: 2 } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.first['name']).to eq(expected_first_branch_name) + + check_merge_status(json_response) + end + end end context 'when repository is disabled' do diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb index 9bfbbe0daab..b5b6ce106e5 100644 --- a/spec/requests/api/broadcast_messages_spec.rb +++ b/spec/requests/api/broadcast_messages_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::BroadcastMessages do +RSpec.describe API::BroadcastMessages do let_it_be(:user) { create(:user) } let_it_be(:admin) { create(:admin) } let_it_be(:message) { create(:broadcast_message) } diff --git a/spec/requests/api/pipeline_schedules_spec.rb b/spec/requests/api/ci/pipeline_schedules_spec.rb index 98eaf36b14e..e0199b7b51c 100644 --- a/spec/requests/api/pipeline_schedules_spec.rb +++ b/spec/requests/api/ci/pipeline_schedules_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::PipelineSchedules do +RSpec.describe API::Ci::PipelineSchedules do let_it_be(:developer) { create(:user) } let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :repository, public_builds: false) } @@ -24,7 +24,7 @@ describe API::PipelineSchedules do .each do |pipeline_schedule| create(:user).tap do |user| project.add_developer(user) - pipeline_schedule.update(owner: user) + pipeline_schedule.update!(owner: user) end pipeline_schedule.pipelines << build(:ci_pipeline, project: project) end diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb index f57223f1de5..c9ca806e2c4 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/ci/pipelines_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Pipelines do +RSpec.describe API::Ci::Pipelines do let_it_be(:user) { create(:user) } let_it_be(:non_member) { create(:user) } @@ -106,11 +106,11 @@ describe API::Pipelines do end end - HasStatus::AVAILABLE_STATUSES.each do |target| + Ci::HasStatus::AVAILABLE_STATUSES.each do |target| context "when status is #{target}" do before do create(:ci_pipeline, project: project, status: target) - exception_status = HasStatus::AVAILABLE_STATUSES - [target] + exception_status = Ci::HasStatus::AVAILABLE_STATUSES - [target] create(:ci_pipeline, project: project, status: exception_status.sample) end diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/ci/runner_spec.rb index 774615757b9..c8718309bf2 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/ci/runner_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Runner, :clean_gitlab_redis_shared_state do +RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do include StubGitlabCalls include RedisHelpers include WorkhorseHelpers @@ -13,7 +13,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state 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_any_instance_of(::Ci::Runner).to receive(:cache_attributes) end describe '/api/v4/runners' do @@ -38,7 +38,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do it 'creates runner with default values' do post api('/runners'), params: { token: registration_token } - runner = Ci::Runner.first + runner = ::Ci::Runner.first expect(response).to have_gitlab_http_status(:created) expect(json_response['id']).to eq(runner.id) @@ -57,7 +57,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do expect(response).to have_gitlab_http_status(:created) expect(project.runners.size).to eq(1) - runner = Ci::Runner.first + runner = ::Ci::Runner.first expect(runner.token).not_to eq(registration_token) expect(runner.token).not_to eq(project.runners_token) expect(runner).to be_project_type @@ -72,7 +72,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do expect(response).to have_gitlab_http_status(:created) expect(group.runners.reload.size).to eq(1) - runner = Ci::Runner.first + runner = ::Ci::Runner.first expect(runner.token).not_to eq(registration_token) expect(runner.token).not_to eq(group.runners_token) expect(runner).to be_group_type @@ -88,7 +88,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do } expect(response).to have_gitlab_http_status(:created) - expect(Ci::Runner.first.description).to eq('server.hostname') + expect(::Ci::Runner.first.description).to eq('server.hostname') end end @@ -100,7 +100,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do } expect(response).to have_gitlab_http_status(:created) - expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2)) + expect(::Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2)) end end @@ -114,8 +114,8 @@ describe API::Runner, :clean_gitlab_redis_shared_state do } expect(response).to have_gitlab_http_status(:created) - expect(Ci::Runner.first.run_untagged).to be false - expect(Ci::Runner.first.tag_list.sort).to eq(['tag']) + expect(::Ci::Runner.first.run_untagged).to be false + expect(::Ci::Runner.first.tag_list.sort).to eq(['tag']) end end @@ -141,7 +141,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do } expect(response).to have_gitlab_http_status(:created) - expect(Ci::Runner.first.locked).to be true + expect(::Ci::Runner.first.locked).to be true end end @@ -154,7 +154,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do } expect(response).to have_gitlab_http_status(:created) - expect(Ci::Runner.first.active).to be true + expect(::Ci::Runner.first.active).to be true end end @@ -166,7 +166,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do } expect(response).to have_gitlab_http_status(:created) - expect(Ci::Runner.first.active).to be false + expect(::Ci::Runner.first.active).to be false end end end @@ -180,7 +180,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do } expect(response).to have_gitlab_http_status(:created) - expect(Ci::Runner.first.ref_protected?).to be true + expect(::Ci::Runner.first.ref_protected?).to be true end end @@ -192,7 +192,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do } expect(response).to have_gitlab_http_status(:created) - expect(Ci::Runner.first.ref_protected?).to be false + expect(::Ci::Runner.first.ref_protected?).to be false end end end @@ -205,7 +205,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do } expect(response).to have_gitlab_http_status(:created) - expect(Ci::Runner.first.maximum_timeout).to eq(9000) + expect(::Ci::Runner.first.maximum_timeout).to eq(9000) end context 'when maximum job timeout is empty' do @@ -216,7 +216,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do } expect(response).to have_gitlab_http_status(:created) - expect(Ci::Runner.first.maximum_timeout).to be_nil + expect(::Ci::Runner.first.maximum_timeout).to be_nil end end end @@ -232,7 +232,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do } expect(response).to have_gitlab_http_status(:created) - expect(Ci::Runner.first.read_attribute(param.to_sym)).to eq(value) + expect(::Ci::Runner.first.read_attribute(param.to_sym)).to eq(value) end end end @@ -243,7 +243,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do headers: { 'X-Forwarded-For' => '123.111.123.111' } expect(response).to have_gitlab_http_status(:created) - expect(Ci::Runner.first.ip_address).to eq('123.111.123.111') + expect(::Ci::Runner.first.ip_address).to eq('123.111.123.111') end end @@ -271,7 +271,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do delete api('/runners'), params: { token: runner.token } expect(response).to have_gitlab_http_status(:no_content) - expect(Ci::Runner.count).to eq(0) + expect(::Ci::Runner.count).to eq(0) end it_behaves_like '412 response' do @@ -518,6 +518,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do request_job info: { platform: :darwin } expect(response).to have_gitlab_http_status(:created) + expect(response.headers['Content-Type']).to eq('application/json') expect(response.headers).not_to have_key('X-GitLab-Last-Update') expect(runner.reload.platform).to eq('darwin') expect(json_response['id']).to eq(job.id) @@ -537,7 +538,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do end it 'creates persistent ref' do - expect_any_instance_of(Ci::PersistentRef).to receive(:create_ref) + expect_any_instance_of(::Ci::PersistentRef).to receive(:create_ref) .with(job.sha, "refs/#{Repository::REF_PIPELINES}/#{job.commit_id}") request_job info: { platform: :darwin } @@ -569,6 +570,24 @@ describe API::Runner, :clean_gitlab_redis_shared_state do end end + context 'when a Gitaly exception is thrown during response' do + before do + allow_next_instance_of(Ci::BuildRunnerPresenter) do |instance| + allow(instance).to receive(:artifacts).and_raise(GRPC::DeadlineExceeded) + end + end + + it 'fails the job as a scheduler failure' do + request_job + + expect(response).to have_gitlab_http_status(:no_content) + expect(job.reload.failed?).to be_truthy + expect(job.failure_reason).to eq('scheduler_failure') + expect(job.runner_id).to eq(runner.id) + expect(job.runner_session).to be_nil + end + end + context 'when GIT_DEPTH is not specified and there is no default git depth for the project' do before do project.update!(ci_default_git_depth: nil) @@ -651,9 +670,9 @@ describe API::Runner, :clean_gitlab_redis_shared_state do context 'when job is for a release' do let!(:job) { create(:ci_build, :release_options, pipeline: pipeline) } - context 'when `release_steps` is passed by the runner' do + context 'when `multi_build_steps` is passed by the runner' do it 'exposes release info' do - request_job info: { features: { release_steps: true } } + request_job info: { features: { multi_build_steps: true } } expect(response).to have_gitlab_http_status(:created) expect(response.headers).not_to have_key('X-GitLab-Last-Update') @@ -668,7 +687,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do { "name" => "release", "script" => - "release-cli create --ref \"$CI_COMMIT_SHA\" --name \"Release $CI_COMMIT_SHA\" --tag-name \"release-$CI_COMMIT_SHA\" --description \"Created using the release-cli $EXTRA_DESCRIPTION\"", + ["release-cli create --name \"Release $CI_COMMIT_SHA\" --description \"Created using the release-cli $EXTRA_DESCRIPTION\" --tag-name \"release-$CI_COMMIT_SHA\" --ref \"$CI_COMMIT_SHA\""], "timeout" => 3600, "when" => "on_success", "allow_failure" => false @@ -677,7 +696,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do end end - context 'when `release_steps` is not passed by the runner' do + context 'when `multi_build_steps` is not passed by the runner' do it 'drops the job' do request_job @@ -749,7 +768,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do context 'when concurrently updating a job' do before do - expect_any_instance_of(Ci::Build).to receive(:run!) + expect_any_instance_of(::Ci::Build).to receive(:run!) .and_raise(ActiveRecord::StaleObjectError.new(nil, nil)) end @@ -890,7 +909,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do let!(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, builds: [job], trigger: trigger) } before do - project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') + project.variables << ::Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') end shared_examples 'expected variables behavior' do @@ -1090,7 +1109,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do def request_job(token = runner.token, **params) new_params = params.merge(token: token, last_update: last_update) - post api('/jobs/request'), params: new_params, headers: { 'User-Agent' => user_agent } + post api('/jobs/request'), params: new_params.to_json, headers: { 'User-Agent' => user_agent, 'Content-Type': 'application/json' } end end @@ -1099,7 +1118,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do let_it_be(:project) { create(:project, :repository) } let(:runner) { create(:ci_runner, :project, projects: [project]) } - let(:service) { Ci::CreateWebIdeTerminalService.new(project, user, ref: 'master').execute } + let(:service) { ::Ci::CreateWebIdeTerminalService.new(project, user, ref: 'master').execute } let(:pipeline) { service[:pipeline] } let(:build) { pipeline.builds.first } let(:job) { {} } @@ -1592,8 +1611,105 @@ describe API::Runner, :clean_gitlab_redis_shared_state do job.run! end + shared_examples_for 'rejecting artifacts that are too large' do + let(:filesize) { 100.megabytes.to_i } + let(:sample_max_size) { (filesize / 1.megabyte) - 10 } # Set max size to be smaller than file size to trigger error + + shared_examples_for 'failed request' do + it 'responds with payload too large error' do + send_request + + expect(response).to have_gitlab_http_status(:payload_too_large) + end + end + + context 'based on plan limit setting' do + let(:application_max_size) { sample_max_size + 100 } + let(:limit_name) { "#{Ci::JobArtifact::PLAN_LIMIT_PREFIX}archive" } + + before do + create(:plan_limits, :default_plan, limit_name => sample_max_size) + stub_application_setting(max_artifacts_size: application_max_size) + end + + context 'and feature flag ci_max_artifact_size_per_type is enabled' do + before do + stub_feature_flags(ci_max_artifact_size_per_type: true) + end + + it_behaves_like 'failed request' + end + + context 'and feature flag ci_max_artifact_size_per_type is disabled' do + before do + stub_feature_flags(ci_max_artifact_size_per_type: false) + end + + it 'bases of project closest setting' do + send_request + + expect(response).to have_gitlab_http_status(success_code) + end + end + end + + context 'based on application setting' do + before do + stub_application_setting(max_artifacts_size: sample_max_size) + end + + it_behaves_like 'failed request' + end + + context 'based on root namespace setting' do + let(:application_max_size) { sample_max_size + 10 } + + before do + stub_application_setting(max_artifacts_size: application_max_size) + root_namespace.update!(max_artifacts_size: sample_max_size) + end + + it_behaves_like 'failed request' + end + + context 'based on child namespace setting' do + let(:application_max_size) { sample_max_size + 10 } + let(:root_namespace_max_size) { sample_max_size + 10 } + + before do + stub_application_setting(max_artifacts_size: application_max_size) + root_namespace.update!(max_artifacts_size: root_namespace_max_size) + namespace.update!(max_artifacts_size: sample_max_size) + end + + it_behaves_like 'failed request' + end + + context 'based on project setting' do + let(:application_max_size) { sample_max_size + 10 } + let(:root_namespace_max_size) { sample_max_size + 10 } + let(:child_namespace_max_size) { sample_max_size + 10 } + + before do + stub_application_setting(max_artifacts_size: application_max_size) + root_namespace.update!(max_artifacts_size: root_namespace_max_size) + namespace.update!(max_artifacts_size: child_namespace_max_size) + project.update!(max_artifacts_size: sample_max_size) + end + + it_behaves_like 'failed request' + end + end + describe 'POST /api/v4/jobs/:id/artifacts/authorize' do context 'when using token as parameter' do + context 'and the artifact is too large' do + it_behaves_like 'rejecting artifacts that are too large' do + let(:success_code) { :ok } + let(:send_request) { authorize_artifacts_with_token_in_params(filesize: filesize) } + end + end + context 'posting artifacts to running job' do subject do authorize_artifacts_with_token_in_params @@ -1651,56 +1767,6 @@ describe API::Runner, :clean_gitlab_redis_shared_state do end end end - - context 'when artifact is too large' do - let(:sample_max_size) { 100 } - - shared_examples_for 'rejecting too large artifacts' do - it 'fails to post' do - authorize_artifacts_with_token_in_params(filesize: sample_max_size.megabytes.to_i) - - expect(response).to have_gitlab_http_status(:payload_too_large) - end - end - - context 'based on application setting' do - before do - stub_application_setting(max_artifacts_size: sample_max_size) - end - - it_behaves_like 'rejecting too large artifacts' - end - - context 'based on root namespace setting' do - before do - stub_application_setting(max_artifacts_size: 200) - root_namespace.update!(max_artifacts_size: sample_max_size) - end - - it_behaves_like 'rejecting too large artifacts' - end - - context 'based on child namespace setting' do - before do - stub_application_setting(max_artifacts_size: 200) - root_namespace.update!(max_artifacts_size: 200) - namespace.update!(max_artifacts_size: sample_max_size) - end - - it_behaves_like 'rejecting too large artifacts' - end - - context 'based on project setting' do - before do - stub_application_setting(max_artifacts_size: 200) - root_namespace.update!(max_artifacts_size: 200) - namespace.update!(max_artifacts_size: 200) - project.update!(max_artifacts_size: sample_max_size) - end - - it_behaves_like 'rejecting too large artifacts' - end - end end context 'when using token as header' do @@ -1757,19 +1823,36 @@ describe API::Runner, :clean_gitlab_redis_shared_state do expect(json_response['ProcessLsif']).to be_truthy end - it 'fails to authorize too large artifact' do - authorize_artifacts_with_token_in_headers(artifact_type: :lsif, filesize: 30.megabytes) + it 'adds ProcessLsifReferences header' do + authorize_artifacts_with_token_in_headers(artifact_type: :lsif) - expect(response).to have_gitlab_http_status(:payload_too_large) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['ProcessLsifReferences']).to be_truthy end context 'code_navigation feature flag is disabled' do - it 'does not add ProcessLsif header' do + it 'responds with a forbidden error' do stub_feature_flags(code_navigation: false) + authorize_artifacts_with_token_in_headers(artifact_type: :lsif) + + aggregate_failures do + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['ProcessLsif']).to be_falsy + expect(json_response['ProcessLsifReferences']).to be_falsy + end + end + end + context 'code_navigation_references feature flag is disabled' do + it 'sets ProcessLsifReferences header to false' do + stub_feature_flags(code_navigation_references: false) authorize_artifacts_with_token_in_headers(artifact_type: :lsif) - expect(response).to have_gitlab_http_status(:forbidden) + aggregate_failures do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['ProcessLsif']).to be_truthy + expect(json_response['ProcessLsifReferences']).to be_falsy + end end end end @@ -1799,6 +1882,32 @@ describe API::Runner, :clean_gitlab_redis_shared_state do expect { upload_artifacts(file_upload, headers_with_token) }.to change { runner.reload.contacted_at } end + context 'when the artifact is too large' do + it_behaves_like 'rejecting artifacts that are too large' do + # This filesize validation also happens in non remote stored files, + # it's just that it's hard to stub the filesize in other cases to be + # more than a megabyte. + let!(:fog_connection) do + stub_artifacts_object_storage(direct_upload: true) + end + let(:object) do + fog_connection.directories.new(key: 'artifacts').files.create( + key: 'tmp/uploads/12312300', + body: 'content' + ) + end + let(:file_upload) { fog_to_uploaded_file(object) } + let(:send_request) do + upload_artifacts(file_upload, headers_with_token, 'file.remote_id' => '12312300') + end + let(:success_code) { :created } + + before do + allow(object).to receive(:content_length).and_return(filesize) + end + end + end + context 'when artifacts are being stored inside of tmp path' do before do # by configuring this path we allow to pass temp file from any path @@ -1877,16 +1986,6 @@ describe API::Runner, :clean_gitlab_redis_shared_state do end end - context 'when artifacts file is too large' do - it 'fails to post too large artifact' do - stub_application_setting(max_artifacts_size: 0) - - upload_artifacts(file_upload, headers_with_token) - - expect(response).to have_gitlab_http_status(:payload_too_large) - end - end - context 'when artifacts post request does not contain file' do it 'fails to post artifacts without file' do post api("/jobs/#{job.id}/artifacts"), params: {}, headers: headers_with_token @@ -2258,7 +2357,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do FileUtils.remove_entry(new_tmpdir) end - it' "fails to post artifacts for outside of tmp path"' do + it 'fails to post artifacts for outside of tmp path' do upload_artifacts(file_upload, headers_with_token) expect(response).to have_gitlab_http_status(:bad_request) diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb index 67c258260bf..670456e5dba 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/ci/runners_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Runners do +RSpec.describe API::Ci::Runners do let_it_be(:admin) { create(:user, :admin) } let_it_be(:user) { create(:user) } let_it_be(:user2) { create(:user) } @@ -266,7 +266,7 @@ describe API::Runners do delete api("/runners/#{unused_project_runner.id}", admin) expect(response).to have_gitlab_http_status(:no_content) - end.to change { Ci::Runner.project_type.count }.by(-1) + end.to change { ::Ci::Runner.project_type.count }.by(-1) end end @@ -284,7 +284,7 @@ describe API::Runners do end end - it 'returns 404 if runner does not exists' do + it 'returns 404 if runner does not exist' do get api('/runners/0', admin) expect(response).to have_gitlab_http_status(:not_found) @@ -437,7 +437,7 @@ describe API::Runners do end end - it 'returns 404 if runner does not exists' do + it 'returns 404 if runner does not exist' do update_runner(0, admin, description: 'test') expect(response).to have_gitlab_http_status(:not_found) @@ -493,7 +493,7 @@ describe API::Runners do delete api("/runners/#{shared_runner.id}", admin) expect(response).to have_gitlab_http_status(:no_content) - end.to change { Ci::Runner.instance_type.count }.by(-1) + end.to change { ::Ci::Runner.instance_type.count }.by(-1) end it_behaves_like '412 response' do @@ -507,11 +507,11 @@ describe API::Runners do delete api("/runners/#{project_runner.id}", admin) expect(response).to have_gitlab_http_status(:no_content) - end.to change { Ci::Runner.project_type.count }.by(-1) + end.to change { ::Ci::Runner.project_type.count }.by(-1) end end - it 'returns 404 if runner does not exists' do + it 'returns 404 if runner does not exist' do delete api('/runners/0', admin) expect(response).to have_gitlab_http_status(:not_found) @@ -542,7 +542,7 @@ describe API::Runners do delete api("/runners/#{project_runner.id}", user) expect(response).to have_gitlab_http_status(:no_content) - end.to change { Ci::Runner.project_type.count }.by(-1) + end.to change { ::Ci::Runner.project_type.count }.by(-1) end it 'does not delete group runner with guest access' do @@ -574,7 +574,7 @@ describe API::Runners do delete api("/runners/#{group_runner_a.id}", user) expect(response).to have_gitlab_http_status(:no_content) - end.to change { Ci::Runner.group_type.count }.by(-1) + end.to change { ::Ci::Runner.group_type.count }.by(-1) end it 'deletes inherited group runner with owner access' do @@ -582,7 +582,7 @@ describe API::Runners do delete api("/runners/#{group_runner_b.id}", user) expect(response).to have_gitlab_http_status(:no_content) - end.to change { Ci::Runner.group_type.count }.by(-1) + end.to change { ::Ci::Runner.group_type.count }.by(-1) end it_behaves_like '412 response' do @@ -968,7 +968,7 @@ describe API::Runners do end it 'does not enable locked runner' do - project_runner2.update(locked: true) + project_runner2.update!(locked: true) expect do post api("/projects/#{project.id}/runners", user), params: { runner_id: project_runner2.id } diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 0c0bf8b4df0..bec15b788c3 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::CommitStatuses do +RSpec.describe API::CommitStatuses do let!(:project) { create(:project, :repository) } let(:commit) { project.repository.commit } let(:guest) { create_user(:guest) } diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index a423c92e2fb..724e3177173 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' require 'mime/types' -describe API::Commits do +RSpec.describe API::Commits do include ProjectForksHelper let(:user) { create(:user) } diff --git a/spec/requests/api/composer_packages_spec.rb b/spec/requests/api/composer_packages_spec.rb new file mode 100644 index 00000000000..d756a7700f6 --- /dev/null +++ b/spec/requests/api/composer_packages_spec.rb @@ -0,0 +1,302 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe API::ComposerPackages do + include PackagesManagerApiSpecHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:group, reload: true) { create(:group, :public) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:package_name) { 'package-name' } + let_it_be(:project, reload: true) { create(:project, :custom_repo, files: { 'composer.json' => { name: package_name }.to_json }, group: group) } + let(:headers) { {} } + + describe 'GET /api/v4/group/:id/-/packages/composer/packages' do + let(:url) { "/group/#{group.id}/-/packages/composer/packages.json" } + + subject { get api(url), headers: headers } + + context 'without the need for a license' do + context 'with valid project' do + let!(:package) { create(:composer_package, :with_metadatum, project: project) } + + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'Composer package index' | :success + 'PUBLIC' | :guest | true | true | 'Composer package index' | :success + 'PUBLIC' | :developer | true | false | 'Composer package index' | :success + 'PUBLIC' | :guest | true | false | 'Composer package index' | :success + 'PUBLIC' | :developer | false | true | 'Composer package index' | :success + 'PUBLIC' | :guest | false | true | 'Composer package index' | :success + 'PUBLIC' | :developer | false | false | 'Composer package index' | :success + 'PUBLIC' | :guest | false | false | 'Composer package index' | :success + 'PUBLIC' | :anonymous | false | true | 'Composer package index' | :success + 'PRIVATE' | :developer | true | true | 'Composer package index' | :success + 'PRIVATE' | :guest | true | true | 'Composer package index' | :success + 'PRIVATE' | :developer | true | false | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | true | false | 'process Composer api request' | :not_found + 'PRIVATE' | :developer | false | true | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | false | true | 'process Composer api request' | :not_found + 'PRIVATE' | :developer | false | false | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | false | false | 'process Composer api request' | :not_found + 'PRIVATE' | :anonymous | false | true | 'process Composer api request' | :not_found + end + + with_them do + include_context 'Composer api group access', params[:project_visibility_level], params[:user_role], params[:user_token] do + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + end + + it_behaves_like 'rejects Composer access with unknown group id' + end + end + + describe 'GET /api/v4/group/:id/-/packages/composer/p/:sha.json' do + let(:sha) { '123' } + let(:url) { "/group/#{group.id}/-/packages/composer/p/#{sha}.json" } + let!(:package) { create(:composer_package, :with_metadatum, project: project) } + + subject { get api(url), headers: headers } + + context 'without the need for a license' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'Composer provider index' | :success + 'PUBLIC' | :guest | true | true | 'Composer provider index' | :success + 'PUBLIC' | :developer | true | false | 'Composer provider index' | :success + 'PUBLIC' | :guest | true | false | 'Composer provider index' | :success + 'PUBLIC' | :developer | false | true | 'Composer provider index' | :success + 'PUBLIC' | :guest | false | true | 'Composer provider index' | :success + 'PUBLIC' | :developer | false | false | 'Composer provider index' | :success + 'PUBLIC' | :guest | false | false | 'Composer provider index' | :success + 'PUBLIC' | :anonymous | false | true | 'Composer provider index' | :success + 'PRIVATE' | :developer | true | true | 'Composer provider index' | :success + 'PRIVATE' | :guest | true | true | 'Composer empty provider index' | :success + 'PRIVATE' | :developer | true | false | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | true | false | 'process Composer api request' | :not_found + 'PRIVATE' | :developer | false | true | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | false | true | 'process Composer api request' | :not_found + 'PRIVATE' | :developer | false | false | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | false | false | 'process Composer api request' | :not_found + 'PRIVATE' | :anonymous | false | true | 'process Composer api request' | :not_found + end + + with_them do + include_context 'Composer api group access', params[:project_visibility_level], params[:user_role], params[:user_token] do + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + end + + it_behaves_like 'rejects Composer access with unknown group id' + end + end + + describe 'GET /api/v4/group/:id/-/packages/composer/*package_name.json' do + let(:package_name) { 'foobar' } + let(:url) { "/group/#{group.id}/-/packages/composer/#{package_name}.json" } + + subject { get api(url), headers: headers } + + context 'without the need for a license' do + context 'with no packages' do + include_context 'Composer user type', :developer, true do + it_behaves_like 'returning response status', :not_found + end + end + + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + let!(:package) { create(:composer_package, :with_metadatum, name: package_name, project: project) } + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'Composer package api request' | :success + 'PUBLIC' | :guest | true | true | 'Composer package api request' | :success + 'PUBLIC' | :developer | true | false | 'Composer package api request' | :success + 'PUBLIC' | :guest | true | false | 'Composer package api request' | :success + 'PUBLIC' | :developer | false | true | 'Composer package api request' | :success + 'PUBLIC' | :guest | false | true | 'Composer package api request' | :success + 'PUBLIC' | :developer | false | false | 'Composer package api request' | :success + 'PUBLIC' | :guest | false | false | 'Composer package api request' | :success + 'PUBLIC' | :anonymous | false | true | 'Composer package api request' | :success + 'PRIVATE' | :developer | true | true | 'Composer package api request' | :success + 'PRIVATE' | :guest | true | true | 'process Composer api request' | :not_found + 'PRIVATE' | :developer | true | false | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | true | false | 'process Composer api request' | :not_found + 'PRIVATE' | :developer | false | true | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | false | true | 'process Composer api request' | :not_found + 'PRIVATE' | :developer | false | false | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | false | false | 'process Composer api request' | :not_found + 'PRIVATE' | :anonymous | false | true | 'process Composer api request' | :not_found + end + + with_them do + include_context 'Composer api group access', params[:project_visibility_level], params[:user_role], params[:user_token] do + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + end + + it_behaves_like 'rejects Composer access with unknown group id' + end + end + + describe 'POST /api/v4/projects/:id/packages/composer' do + let(:url) { "/projects/#{project.id}/packages/composer" } + let(:params) { {} } + + before(:all) do + project.repository.add_tag(user, 'v1.2.99', 'master') + end + + subject { post api(url), headers: headers, params: params } + + shared_examples 'composer package publish' do + context 'without the need for a license' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'Composer package creation' | :created + 'PUBLIC' | :guest | true | true | 'process Composer api request' | :forbidden + 'PUBLIC' | :developer | true | false | 'process Composer api request' | :unauthorized + 'PUBLIC' | :guest | true | false | 'process Composer api request' | :unauthorized + 'PUBLIC' | :developer | false | true | 'process Composer api request' | :forbidden + 'PUBLIC' | :guest | false | true | 'process Composer api request' | :forbidden + 'PUBLIC' | :developer | false | false | 'process Composer api request' | :unauthorized + 'PUBLIC' | :guest | false | false | 'process Composer api request' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'process Composer api request' | :unauthorized + 'PRIVATE' | :developer | true | true | 'Composer package creation' | :created + 'PRIVATE' | :guest | true | true | 'process Composer api request' | :forbidden + 'PRIVATE' | :developer | true | false | 'process Composer api request' | :unauthorized + 'PRIVATE' | :guest | true | false | 'process Composer api request' | :unauthorized + 'PRIVATE' | :developer | false | true | 'process Composer api request' | :not_found + 'PRIVATE' | :guest | false | true | 'process Composer api request' | :not_found + 'PRIVATE' | :developer | false | false | 'process Composer api request' | :unauthorized + 'PRIVATE' | :guest | false | false | 'process Composer api request' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'process Composer api request' | :unauthorized + end + + with_them do + include_context 'Composer api project access', params[:project_visibility_level], params[:user_role], params[:user_token] do + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + end + + it_behaves_like 'rejects Composer access with unknown project id' + end + end + + context 'with no tag or branch params' do + let(:headers) { build_basic_auth_header(user.username, personal_access_token.token) } + + it_behaves_like 'process Composer api request', :developer, :bad_request + end + + context 'with a tag' do + context 'with an existing branch' do + let(:params) { { tag: 'v1.2.99' } } + + it_behaves_like 'composer package publish' + end + + context 'with a non existing tag' do + let(:params) { { tag: 'non-existing-tag' } } + let(:headers) { build_basic_auth_header(user.username, personal_access_token.token) } + + it_behaves_like 'process Composer api request', :developer, :not_found + end + end + + context 'with a branch' do + context 'with an existing branch' do + let(:params) { { branch: 'master' } } + + it_behaves_like 'composer package publish' + end + + context 'with a non existing branch' do + let(:params) { { branch: 'non-existing-branch' } } + let(:headers) { build_basic_auth_header(user.username, personal_access_token.token) } + + it_behaves_like 'process Composer api request', :developer, :not_found + end + end + end + + describe 'GET /api/v4/projects/:id/packages/composer/archives/*package_name?sha=:sha' do + let(:sha) { '123' } + let(:url) { "/projects/#{project.id}/packages/composer/archives/#{package_name}.zip" } + let(:params) { { sha: sha } } + + subject { get api(url), headers: headers, params: params } + + context 'without the need for a license' do + context 'with valid project' do + let!(:package) { create(:composer_package, :with_metadatum, name: package_name, project: project) } + + context 'when the sha does not match the package name' do + let(:sha) { '123' } + + it_behaves_like 'process Composer api request', :anonymous, :not_found + end + + context 'when the package name does not match the sha' do + let(:branch) { project.repository.find_branch('master') } + let(:sha) { branch.target } + let(:url) { "/projects/#{project.id}/packages/composer/archives/unexisting-package-name.zip" } + + it_behaves_like 'process Composer api request', :anonymous, :not_found + end + + context 'with a match package name and sha' do + let(:branch) { project.repository.find_branch('master') } + let(:sha) { branch.target } + + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :expected_status) do + 'PUBLIC' | :developer | true | true | :success + 'PUBLIC' | :guest | true | true | :success + 'PUBLIC' | :developer | true | false | :success + 'PUBLIC' | :guest | true | false | :success + 'PUBLIC' | :developer | false | true | :success + 'PUBLIC' | :guest | false | true | :success + 'PUBLIC' | :developer | false | false | :success + 'PUBLIC' | :guest | false | false | :success + 'PUBLIC' | :anonymous | false | true | :success + 'PRIVATE' | :developer | true | true | :success + 'PRIVATE' | :guest | true | true | :success + 'PRIVATE' | :developer | true | false | :success + 'PRIVATE' | :guest | true | false | :success + 'PRIVATE' | :developer | false | true | :success + 'PRIVATE' | :guest | false | true | :success + 'PRIVATE' | :developer | false | false | :success + 'PRIVATE' | :guest | false | false | :success + 'PRIVATE' | :anonymous | false | true | :success + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like 'process Composer api request', params[:user_role], params[:expected_status], params[:member] + end + end + end + + it_behaves_like 'rejects Composer access with unknown project id' + end + end +end diff --git a/spec/requests/api/conan_packages_spec.rb b/spec/requests/api/conan_packages_spec.rb new file mode 100644 index 00000000000..1d88eaef79c --- /dev/null +++ b/spec/requests/api/conan_packages_spec.rb @@ -0,0 +1,840 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe API::ConanPackages do + include WorkhorseHelpers + include PackagesManagerApiSpecHelpers + + let(:package) { create(:conan_package) } + let_it_be(:personal_access_token) { create(:personal_access_token) } + let_it_be(:user) { personal_access_token.user } + let(:project) { package.project } + + let(:base_secret) { SecureRandom.base64(64) } + let(:auth_token) { personal_access_token.token } + let(:job) { create(:ci_build, user: user) } + let(:job_token) { job.token } + let(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } + let(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } + + let(:headers) do + { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', auth_token) } + end + + let(:jwt_secret) do + OpenSSL::HMAC.hexdigest( + OpenSSL::Digest::SHA256.new, + base_secret, + Gitlab::ConanToken::HMAC_KEY + ) + end + + before do + project.add_developer(user) + allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret) + end + + describe 'GET /api/v4/packages/conan/v1/ping' do + it 'responds with 401 Unauthorized when no token provided' do + get api('/packages/conan/v1/ping') + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'responds with 200 OK when valid token is provided' do + jwt = build_jwt(personal_access_token) + get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['X-Conan-Server-Capabilities']).to eq("") + end + + it 'responds with 200 OK when valid job token is provided' do + jwt = build_jwt_from_job(job) + get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['X-Conan-Server-Capabilities']).to eq("") + end + + it 'responds with 200 OK when valid deploy token is provided' do + jwt = build_jwt_from_deploy_token(deploy_token) + get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['X-Conan-Server-Capabilities']).to eq("") + end + + it 'responds with 401 Unauthorized when invalid access token ID is provided' do + jwt = build_jwt(double(id: 12345), user_id: personal_access_token.user_id) + get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'responds with 401 Unauthorized when invalid user is provided' do + jwt = build_jwt(personal_access_token, user_id: 12345) + get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'responds with 401 Unauthorized when the provided JWT is signed with different secret' do + jwt = build_jwt(personal_access_token, secret: SecureRandom.base64(32)) + get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'responds with 401 Unauthorized when invalid JWT is provided' do + get api('/packages/conan/v1/ping'), headers: build_token_auth_header('invalid-jwt') + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + context 'packages feature disabled' do + it 'responds with 404 Not Found' do + stub_packages_setting(enabled: false) + get api('/packages/conan/v1/ping') + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + describe 'GET /api/v4/packages/conan/v1/conans/search' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + + get api('/packages/conan/v1/conans/search'), headers: headers, params: params + end + + subject { json_response['results'] } + + context 'returns packages with a matching name' do + let(:params) { { q: package.conan_recipe } } + + it { is_expected.to contain_exactly(package.conan_recipe) } + end + + context 'returns packages using a * wildcard' do + let(:params) { { q: "#{package.name[0, 3]}*" } } + + it { is_expected.to contain_exactly(package.conan_recipe) } + end + + context 'does not return non-matching packages' do + let(:params) { { q: "foo" } } + + it { is_expected.to be_blank } + end + end + + describe 'GET /api/v4/packages/conan/v1/users/authenticate' do + subject { get api('/packages/conan/v1/users/authenticate'), headers: headers } + + context 'when using invalid token' do + let(:auth_token) { 'invalid_token' } + + it 'responds with 401' do + subject + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'when valid JWT access token is provided' do + it 'responds with 200' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'token has valid validity time' do + Timecop.freeze do + subject + + payload = JSONWebToken::HMACToken.decode( + response.body, jwt_secret).first + expect(payload['access_token']).to eq(personal_access_token.id) + expect(payload['user_id']).to eq(personal_access_token.user_id) + + duration = payload['exp'] - payload['iat'] + expect(duration).to eq(1.hour) + end + end + end + + context 'with valid job token' do + let(:auth_token) { job_token } + + it 'responds with 200' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'with valid deploy token' do + let(:auth_token) { deploy_token.token } + + it 'responds with 200' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + describe 'GET /api/v4/packages/conan/v1/users/check_credentials' do + it 'responds with a 200 OK with PAT' do + get api('/packages/conan/v1/users/check_credentials'), headers: headers + + expect(response).to have_gitlab_http_status(:ok) + end + + context 'with job token' do + let(:auth_token) { job_token } + + it 'responds with a 200 OK with job token' do + get api('/packages/conan/v1/users/check_credentials'), headers: headers + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'with deploy token' do + let(:auth_token) { deploy_token.token } + + it 'responds with a 200 OK with job token' do + get api('/packages/conan/v1/users/check_credentials'), headers: headers + + expect(response).to have_gitlab_http_status(:ok) + end + end + + it 'responds with a 401 Unauthorized when an invalid token is used' do + get api('/packages/conan/v1/users/check_credentials'), headers: build_token_auth_header('invalid-token') + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + shared_examples 'rejects invalid recipe' do + context 'with invalid recipe path' do + let(:recipe_path) { '../../foo++../..' } + + it 'returns 400' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end + + shared_examples 'rejects recipe for invalid project' do + context 'with invalid recipe path' do + let(:recipe_path) { 'aa/bb/not-existing-project/ccc' } + + it 'returns forbidden' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + shared_examples 'rejects recipe for not found package' do + context 'with invalid recipe path' do + let(:recipe_path) do + 'aa/bb/%{project}/ccc' % { project: ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) } + end + + it 'returns not found' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + shared_examples 'empty recipe for not found package' do + context 'with invalid recipe url' do + let(:recipe_path) do + 'aa/bb/%{project}/ccc' % { project: ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) } + end + + it 'returns not found' do + allow(::Packages::Conan::PackagePresenter).to receive(:new) + .with( + 'aa/bb@%{project}/ccc' % { project: ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) }, + user, + project, + any_args + ).and_return(presenter) + allow(presenter).to receive(:recipe_snapshot) { {} } + allow(presenter).to receive(:package_snapshot) { {} } + + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to eq("{}") + end + end + end + + shared_examples 'recipe download_urls' do + let(:recipe_path) { package.conan_recipe_path } + + it 'returns the download_urls for the recipe files' do + expected_response = { + 'conanfile.py' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py", + 'conanmanifest.txt' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt" + } + + allow(presenter).to receive(:recipe_urls) { expected_response } + + subject + + expect(json_response).to eq(expected_response) + end + end + + shared_examples 'package download_urls' do + let(:recipe_path) { package.conan_recipe_path } + + it 'returns the download_urls for the package files' do + expected_response = { + 'conaninfo.txt' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt", + 'conanmanifest.txt' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt", + 'conan_package.tgz' => "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz" + } + + allow(presenter).to receive(:package_urls) { expected_response } + + subject + + expect(json_response).to eq(expected_response) + end + end + + context 'recipe endpoints' do + let(:jwt) { build_jwt(personal_access_token) } + let(:headers) { build_token_auth_header(jwt.encoded) } + let(:conan_package_reference) { '123456789' } + let(:presenter) { double('::Packages::Conan::PackagePresenter') } + + before do + allow(::Packages::Conan::PackagePresenter).to receive(:new) + .with(package.conan_recipe, user, package.project, any_args) + .and_return(presenter) + end + + describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do + let(:recipe_path) { package.conan_recipe_path } + + subject { get api("/packages/conan/v1/conans/#{recipe_path}"), headers: headers } + + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'rejects recipe for invalid project' + it_behaves_like 'empty recipe for not found package' + + context 'with existing package' do + it 'returns a hash of files with their md5 hashes' do + expected_response = { + 'conanfile.py' => 'md5hash1', + 'conanmanifest.txt' => 'md5hash2' + } + + allow(presenter).to receive(:recipe_snapshot) { expected_response } + + subject + + expect(json_response).to eq(expected_response) + end + end + end + + describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference' do + let(:recipe_path) { package.conan_recipe_path } + + subject { get api("/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}"), headers: headers } + + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'rejects recipe for invalid project' + it_behaves_like 'empty recipe for not found package' + + context 'with existing package' do + it 'returns a hash of md5 values for the files' do + expected_response = { + 'conaninfo.txt' => "md5hash1", + 'conanmanifest.txt' => "md5hash2", + 'conan_package.tgz' => "md5hash3" + } + + allow(presenter).to receive(:package_snapshot) { expected_response } + + subject + + expect(json_response).to eq(expected_response) + end + end + end + + describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/digest' do + subject { get api("/packages/conan/v1/conans/#{recipe_path}/digest"), headers: headers } + + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'rejects recipe for invalid project' + it_behaves_like 'recipe download_urls' + end + + describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/download_urls' do + subject { get api("/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/download_urls"), headers: headers } + + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'rejects recipe for invalid project' + it_behaves_like 'package download_urls' + end + + describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/download_urls' do + subject { get api("/packages/conan/v1/conans/#{recipe_path}/download_urls"), headers: headers } + + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'rejects recipe for invalid project' + it_behaves_like 'recipe download_urls' + end + + describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/digest' do + subject { get api("/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/digest"), headers: headers } + + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'rejects recipe for invalid project' + it_behaves_like 'package download_urls' + end + + describe 'POST /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/upload_urls' do + let(:recipe_path) { package.conan_recipe_path } + + let(:params) do + { "conanfile.py": 24, + "conanmanifext.txt": 123 } + end + + subject { post api("/packages/conan/v1/conans/#{recipe_path}/upload_urls"), params: params, headers: headers } + + it_behaves_like 'rejects invalid recipe' + + it 'returns a set of upload urls for the files requested' do + subject + + expected_response = { + 'conanfile.py': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py", + 'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt" + } + + expect(response.body).to eq(expected_response.to_json) + end + end + + describe 'POST /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/upload_urls' do + let(:recipe_path) { package.conan_recipe_path } + + let(:params) do + { "conaninfo.txt": 24, + "conanmanifext.txt": 123, + "conan_package.tgz": 523 } + end + + subject { post api("/packages/conan/v1/conans/#{recipe_path}/packages/123456789/upload_urls"), params: params, headers: headers } + + it_behaves_like 'rejects invalid recipe' + + it 'returns a set of upload urls for the files requested' do + expected_response = { + 'conaninfo.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt", + 'conanmanifest.txt': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt", + 'conan_package.tgz': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz" + } + + subject + + expect(response.body).to eq(expected_response.to_json) + end + end + + describe 'DELETE /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do + let(:recipe_path) { package.conan_recipe_path } + + subject { delete api("/packages/conan/v1/conans/#{recipe_path}"), headers: headers} + + it_behaves_like 'rejects invalid recipe' + + it 'returns unauthorized for users without valid permission' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + + context 'with delete permissions' do + before do + project.add_maintainer(user) + end + + it_behaves_like 'a gitlab tracking event', described_class.name, 'delete_package' + + it 'deletes a package' do + expect { subject }.to change { Packages::Package.count }.from(2).to(1) + end + end + end + end + + context 'file endpoints' do + let(:jwt) { build_jwt(personal_access_token) } + let(:headers) { build_token_auth_header(jwt.encoded) } + let(:recipe_path) { package.conan_recipe_path } + + shared_examples 'denies download with no token' do + context 'with no private token' do + let(:headers) { {} } + + it 'returns 400' do + subject + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + shared_examples 'a public project with packages' do + it 'returns the file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + end + + shared_examples 'an internal project with packages' do + before do + project.team.truncate + project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + end + + it_behaves_like 'denies download with no token' + + it 'returns the file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + end + + shared_examples 'a private project with packages' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + it_behaves_like 'denies download with no token' + + it 'returns the file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + + it 'denies download when not enough permissions' do + project.add_guest(user) + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + shared_examples 'a project is not found' do + let(:recipe_path) { 'not/package/for/project' } + + it 'returns forbidden' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/ +:recipe_revision/export/:file_name' do + let(:recipe_file) { package.package_files.find_by(file_name: 'conanfile.py') } + let(:metadata) { recipe_file.conan_file_metadatum } + + subject do + get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{recipe_file.file_name}"), + headers: headers + end + + it_behaves_like 'a public project with packages' + it_behaves_like 'an internal project with packages' + it_behaves_like 'a private project with packages' + it_behaves_like 'a project is not found' + end + + describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/ +:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do + let(:package_file) { package.package_files.find_by(file_name: 'conaninfo.txt') } + let(:metadata) { package_file.conan_file_metadatum } + + subject do + get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/package/#{metadata.conan_package_reference}/#{metadata.package_revision}/#{package_file.file_name}"), + headers: headers + end + + it_behaves_like 'a public project with packages' + it_behaves_like 'an internal project with packages' + it_behaves_like 'a private project with packages' + it_behaves_like 'a project is not found' + + context 'tracking the conan_package.tgz download' do + let(:package_file) { package.package_files.find_by(file_name: ::Packages::Conan::FileMetadatum::PACKAGE_BINARY) } + + it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package' + end + end + end + + context 'file uploads' do + let(:jwt) { build_jwt(personal_access_token) } + let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } + let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } } + let(:headers_with_token) { build_token_auth_header(jwt.encoded).merge(workhorse_header) } + let(:recipe_path) { "foo/bar/#{project.full_path.tr('/', '+')}/baz"} + + shared_examples 'uploads a package file' do + context 'with object storage disabled' do + context 'without a file from workhorse' do + let(:params) { { file: nil } } + + it_behaves_like 'package workhorse uploads' + + it 'rejects the request' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'without a token' do + it 'rejects request without a token' do + headers_with_token.delete('HTTP_AUTHORIZATION') + + subject + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'when params from workhorse are correct' do + it 'creates package and stores package file' do + expect { subject } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + + expect(response).to have_gitlab_http_status(:ok) + + package_file = project.packages.last.package_files.reload.last + expect(package_file.file_name).to eq(params[:file].original_filename) + end + + it "doesn't attempt to migrate file to object storage" do + expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async) + + subject + end + end + end + + context 'with object storage enabled' do + context 'and direct upload enabled' do + let!(:fog_connection) do + stub_package_file_object_storage(direct_upload: true) + end + + let(:tmp_object) do + fog_connection.directories.new(key: 'packages').files.create( + key: "tmp/uploads/#{file_name}", + body: 'content' + ) + end + + let(:fog_file) { fog_to_uploaded_file(tmp_object) } + + ['123123', '../../123123'].each do |remote_id| + context "with invalid remote_id: #{remote_id}" do + let(:params) do + { + file: fog_file, + 'file.remote_id' => remote_id + } + end + + it 'responds with status 403' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + context 'with valid remote_id' do + let(:params) do + { + file: fog_file, + 'file.remote_id' => file_name + } + end + + it 'creates package and stores package file' do + expect { subject } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + + expect(response).to have_gitlab_http_status(:ok) + + package_file = project.packages.last.package_files.reload.last + expect(package_file.file_name).to eq(params[:file].original_filename) + expect(package_file.file.read).to eq('content') + end + end + end + + it_behaves_like 'background upload schedules a file migration' + end + end + + shared_examples 'workhorse authorization' do + it 'authorizes posting package with a valid token' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + end + + it 'rejects request without a valid token' do + headers_with_token['HTTP_AUTHORIZATION'] = 'foo' + + subject + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'rejects request without a valid permission' do + project.add_guest(user) + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'rejects requests that bypassed gitlab-workhorse' do + headers_with_token.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + + context 'when using remote storage' do + context 'when direct upload is enabled' do + before do + stub_package_file_object_storage(enabled: true, direct_upload: true) + end + + it 'responds with status 200, location of package remote store and object details' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response).not_to have_key('TempPath') + expect(json_response['RemoteObject']).to have_key('ID') + expect(json_response['RemoteObject']).to have_key('GetURL') + expect(json_response['RemoteObject']).to have_key('StoreURL') + expect(json_response['RemoteObject']).to have_key('DeleteURL') + expect(json_response['RemoteObject']).not_to have_key('MultipartUpload') + end + end + + context 'when direct upload is disabled' do + before do + stub_package_file_object_storage(enabled: true, direct_upload: false) + end + + it 'handles as a local file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response['TempPath']).to eq(::Packages::PackageFileUploader.workhorse_local_upload_path) + expect(json_response['RemoteObject']).to be_nil + end + end + end + end + + describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name/authorize' do + subject { put api("/packages/conan/v1/files/#{recipe_path}/0/export/conanfile.py/authorize"), headers: headers_with_token } + + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'workhorse authorization' + end + + describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:conan_package_reference/:package_revision/:file_name/authorize' do + subject { put api("/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/conaninfo.txt/authorize"), headers: headers_with_token } + + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'workhorse authorization' + end + + describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name' do + let(:file_name) { 'conanfile.py' } + let(:params) { { file: temp_file(file_name) } } + + subject do + workhorse_finalize( + "/api/v4/packages/conan/v1/files/#{recipe_path}/0/export/#{file_name}", + method: :put, + file_key: :file, + params: params, + headers: headers_with_token + ) + end + + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'uploads a package file' + end + + describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:conan_package_reference/:package_revision/:file_name' do + let(:file_name) { 'conaninfo.txt' } + let(:params) { { file: temp_file(file_name) } } + + subject do + workhorse_finalize( + "/api/v4/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/#{file_name}", + method: :put, + file_key: :file, + params: params, + headers: headers_with_token + ) + end + + it_behaves_like 'rejects invalid recipe' + it_behaves_like 'uploads a package file' + context 'tracking the conan_package.tgz upload' do + let(:file_name) { ::Packages::Conan::FileMetadatum::PACKAGE_BINARY } + + it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package' + end + end + end +end diff --git a/spec/requests/api/container_registry_event_spec.rb b/spec/requests/api/container_registry_event_spec.rb index 2cdf2656cb7..4d38ddddffd 100644 --- a/spec/requests/api/container_registry_event_spec.rb +++ b/spec/requests/api/container_registry_event_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::ContainerRegistryEvent do +RSpec.describe API::ContainerRegistryEvent do let(:secret_token) { 'secret_token' } let(:events) { [{ action: 'push' }] } let(:registry_headers) { { 'Content-Type' => ::API::ContainerRegistryEvent::DOCKER_DISTRIBUTION_EVENTS_V1_JSON } } diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index e8cc6bc71ae..81cef653770 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::DeployKeys do +RSpec.describe API::DeployKeys do let(:user) { create(:user) } let(:maintainer) { create(:user) } let(:admin) { create(:admin) } diff --git a/spec/requests/api/deploy_tokens_spec.rb b/spec/requests/api/deploy_tokens_spec.rb index 2b86d59fbba..8ec4f888e2e 100644 --- a/spec/requests/api/deploy_tokens_spec.rb +++ b/spec/requests/api/deploy_tokens_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::DeployTokens do +RSpec.describe API::DeployTokens do let_it_be(:user) { create(:user) } let_it_be(:creator) { create(:user) } let_it_be(:project) { create(:project, creator_id: creator.id) } diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index ef2415a0cde..8113de96ac4 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Deployments do +RSpec.describe API::Deployments do let(:user) { create(:user) } let(:non_member) { create(:user) } diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb index cb3efb2cf5f..720ea429c2c 100644 --- a/spec/requests/api/discussions_spec.rb +++ b/spec/requests/api/discussions_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Discussions do +RSpec.describe API::Discussions do let(:user) { create(:user) } let!(:project) { create(:project, :public, :repository, namespace: user.namespace) } let(:private_user) { create(:user) } diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb index a25a6485f47..f16cd58bb34 100644 --- a/spec/requests/api/doorkeeper_access_spec.rb +++ b/spec/requests/api/doorkeeper_access_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'doorkeeper access' do +RSpec.describe 'doorkeeper 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/environments_spec.rb b/spec/requests/api/environments_spec.rb index 91b3dd93433..b1ac8f9eeec 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Environments do +RSpec.describe API::Environments do let(:user) { create(:user) } let(:non_member) { create(:user) } let(:project) { create(:project, :private, :repository, namespace: user.namespace) } diff --git a/spec/requests/api/error_tracking_spec.rb b/spec/requests/api/error_tracking_spec.rb index deed9777025..8c9ca1b6a9d 100644 --- a/spec/requests/api/error_tracking_spec.rb +++ b/spec/requests/api/error_tracking_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::ErrorTracking do +RSpec.describe API::ErrorTracking do let_it_be(:user) { create(:user) } let(:setting) { create(:project_error_tracking_setting) } let(:project) { setting.project } diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb index 58a55c2e6d0..6a8d5f91abd 100644 --- a/spec/requests/api/events_spec.rb +++ b/spec/requests/api/events_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Events do +RSpec.describe API::Events do let(:user) { create(:user) } let(:non_member) { create(:user) } let(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) } diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb index 59a9ed2f77d..2746e777306 100644 --- a/spec/requests/api/features_spec.rb +++ b/spec/requests/api/features_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Features, stub_feature_flags: false do +RSpec.describe API::Features, stub_feature_flags: false do let_it_be(:user) { create(:user) } let_it_be(:admin) { create(:admin) } diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 198e4f64bcc..b50f63ed67c 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -2,7 +2,9 @@ require 'spec_helper' -describe API::Files do +RSpec.describe API::Files do + include RepoHelpers + let(:user) { create(:user) } let!(:project) { create(:project, :repository, namespace: user.namespace ) } let(:guest) { create(:user) { |u| project.add_guest(u) } } @@ -183,6 +185,26 @@ describe API::Files do expect(response.content_type).to eq('application/json') end + context 'with filename with pathspec characters' do + let(:file_path) { ':wq' } + let(:newrev) { project.repository.commit('master').sha } + + before do + create_file_in_repo(project, 'master', 'master', file_path, 'Test file') + end + + it 'returns JSON wth commit SHA' do + params[:ref] = 'master' + + get api(route(file_path), api_user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['file_path']).to eq(file_path) + expect(json_response['file_name']).to eq(file_path) + expect(json_response['last_commit_id']).to eq(newrev) + end + end + it 'returns file by commit sha' do # This file is deleted on HEAD file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee" diff --git a/spec/requests/api/freeze_periods_spec.rb b/spec/requests/api/freeze_periods_spec.rb index 0b7828ebedf..5589d4d543d 100644 --- a/spec/requests/api/freeze_periods_spec.rb +++ b/spec/requests/api/freeze_periods_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::FreezePeriods do +RSpec.describe API::FreezePeriods do let_it_be(:project) { create(:project, :repository, :private) } let_it_be(:user) { create(:user) } let_it_be(:admin) { create(:admin) } diff --git a/spec/requests/api/go_proxy_spec.rb b/spec/requests/api/go_proxy_spec.rb new file mode 100644 index 00000000000..91e455dac19 --- /dev/null +++ b/spec/requests/api/go_proxy_spec.rb @@ -0,0 +1,465 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::GoProxy do + include PackagesManagerApiSpecHelpers + + let_it_be(:user) { create :user } + let_it_be(:project) { create :project_empty_repo, creator: user, path: 'my-go-lib' } + let_it_be(:base) { "#{Settings.build_gitlab_go_url}/#{project.full_path}" } + + let_it_be(:oauth) { create :oauth_access_token, scopes: 'api', resource_owner: user } + let_it_be(:job) { create :ci_build, user: user } + let_it_be(:pa_token) { create :personal_access_token, user: user } + + let_it_be(:modules) do + commits = [ + create(:go_module_commit, :files, project: project, tag: 'v1.0.0', files: { 'README.md' => 'Hi' } ), + create(:go_module_commit, :module, project: project, tag: 'v1.0.1' ), + create(:go_module_commit, :package, project: project, tag: 'v1.0.2', path: 'pkg' ), + create(:go_module_commit, :module, project: project, tag: 'v1.0.3', name: 'mod' ), + create(:go_module_commit, :files, project: project, files: { 'y.go' => "package a\n" } ), + create(:go_module_commit, :module, project: project, name: 'v2' ), + create(:go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/x.go' => "package a\n" }) + ] + + { sha: [commits[4].sha, commits[5].sha] } + end + + before do + project.add_developer(user) + + stub_feature_flags(go_proxy_disable_gomod_validation: false) + + modules + end + + shared_examples 'an unavailable resource' do + it 'returns not found' do + get_resource(user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + shared_examples 'a module version list resource' do |*versions, path: ''| + let(:module_name) { "#{base}#{path}" } + let(:resource) { "list" } + + it "returns #{versions.empty? ? 'nothing' : versions.join(', ')}" do + get_resource(user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body.split("\n").to_set).to eq(versions.to_set) + end + end + + shared_examples 'a missing module version list resource' do |path: ''| + let(:module_name) { "#{base}#{path}" } + let(:resource) { "list" } + + it_behaves_like 'an unavailable resource' + end + + shared_examples 'a module version information resource' do |version, path: ''| + let(:module_name) { "#{base}#{path}" } + let(:resource) { "#{version}.info" } + + it "returns information for #{version}" do + get_resource(user) + + time = project.repository.find_tag(version).dereferenced_target.committed_date + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_kind_of(Hash) + expect(json_response['Version']).to eq(version) + expect(json_response['Time']).to eq(time.strftime('%Y-%m-%dT%H:%M:%S.%L%:z')) + end + end + + shared_examples 'a missing module version information resource' do |version, path: ''| + let(:module_name) { "#{base}#{path}" } + let(:resource) { "#{version}.info" } + + it_behaves_like 'an unavailable resource' + end + + shared_examples 'a module pseudo-version information resource' do |prefix, path: ''| + let(:module_name) { "#{base}#{path}" } + let(:commit) { project.repository.commit_by(oid: sha) } + let(:version) { fmt_pseudo_version prefix, commit } + let(:resource) { "#{version}.info" } + + it "returns information for #{prefix}yyyymmddhhmmss-abcdefabcdef" do + get_resource(user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_kind_of(Hash) + expect(json_response['Version']).to eq(version) + expect(json_response['Time']).to eq(commit.committed_date.strftime('%Y-%m-%dT%H:%M:%S.%L%:z')) + end + end + + shared_examples 'a missing module pseudo-version information resource' do |path: ''| + let(:module_name) { "#{base}#{path}" } + let(:commit) do + raise "tried to reference :commit without defining :sha" unless defined?(sha) + + project.repository.commit_by(oid: sha) + end + let(:resource) { "#{version}.info" } + + it_behaves_like 'an unavailable resource' + end + + shared_examples 'a module file resource' do |version, path: ''| + let(:module_name) { "#{base}#{path}" } + let(:resource) { "#{version}.mod" } + + it "returns #{path}/go.mod from the repo" do + get_resource(user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body.split("\n", 2).first).to eq("module #{module_name}") + end + end + + shared_examples 'a missing module file resource' do |version, path: ''| + let(:module_name) { "#{base}#{path}" } + let(:resource) { "#{version}.mod" } + + it_behaves_like 'an unavailable resource' + end + + shared_examples 'a module archive resource' do |version, entries, path: ''| + let(:module_name) { "#{base}#{path}" } + let(:resource) { "#{version}.zip" } + + it "returns an archive of #{path.empty? ? '/' : path} @ #{version} from the repo" do + get_resource(user) + + expect(response).to have_gitlab_http_status(:ok) + + entries = entries.map { |e| "#{module_name}@#{version}/#{e}" }.to_set + actual = Set[] + Zip::InputStream.open(StringIO.new(response.body)) do |zip| + while (entry = zip.get_next_entry) + actual.add(entry.name) + end + end + + expect(actual).to eq(entries) + end + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/list' do + context 'for the root module' do + it_behaves_like 'a module version list resource', 'v1.0.1', 'v1.0.2', 'v1.0.3' + end + + context 'for the package' do + it_behaves_like 'a module version list resource', path: '/pkg' + end + + context 'for the submodule' do + it_behaves_like 'a module version list resource', 'v1.0.3', path: '/mod' + end + + context 'for the root module v2' do + it_behaves_like 'a module version list resource', 'v2.0.0', path: '/v2' + end + + context 'with a URL encoded relative path component' do + it_behaves_like 'a missing module version list resource', path: '/%2E%2E%2Fxyz' + end + + context 'with the feature disabled' do + before do + stub_feature_flags(go_proxy: false) + end + + it_behaves_like 'a missing module version list resource' + end + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do + context 'with the root module v1.0.1' do + it_behaves_like 'a module version information resource', 'v1.0.1' + end + + context 'with the submodule v1.0.3' do + it_behaves_like 'a module version information resource', 'v1.0.3', path: '/mod' + end + + context 'with the root module v2.0.0' do + it_behaves_like 'a module version information resource', 'v2.0.0', path: '/v2' + end + + context 'with an invalid path' do + it_behaves_like 'a missing module version information resource', 'v1.0.3', path: '/pkg' + end + + context 'with an invalid version' do + it_behaves_like 'a missing module version information resource', 'v1.0.1', path: '/mod' + end + + context 'with a pseudo-version for v1' do + it_behaves_like 'a module pseudo-version information resource', 'v1.0.4-0.' do + let(:sha) { modules[:sha][0] } + end + end + + context 'with a pseudo-version for v2' do + it_behaves_like 'a module pseudo-version information resource', 'v2.0.0-', path: '/v2' do + let(:sha) { modules[:sha][1] } + end + end + + context 'with a pseudo-version with an invalid timestamp' do + it_behaves_like 'a missing module pseudo-version information resource' do + let(:version) { "v1.0.4-0.00000000000000-#{modules[:sha][0][0..11]}" } + end + end + + context 'with a pseudo-version with an invalid commit sha' do + it_behaves_like 'a missing module pseudo-version information resource' do + let(:sha) { modules[:sha][0] } + let(:version) { "v1.0.4-0.#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-000000000000" } + end + end + + context 'with a pseudo-version with a short commit sha' do + it_behaves_like 'a missing module pseudo-version information resource' do + let(:sha) { modules[:sha][0] } + let(:version) { "v1.0.4-0.#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{modules[:sha][0][0..10]}" } + end + end + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.mod' do + context 'with the root module v1.0.1' do + it_behaves_like 'a module file resource', 'v1.0.1' + end + + context 'with the submodule v1.0.3' do + it_behaves_like 'a module file resource', 'v1.0.3', path: '/mod' + end + + context 'with the root module v2.0.0' do + it_behaves_like 'a module file resource', 'v2.0.0', path: '/v2' + end + + context 'with an invalid path' do + it_behaves_like 'a missing module file resource', 'v1.0.3', path: '/pkg' + end + + context 'with an invalid version' do + it_behaves_like 'a missing module file resource', 'v1.0.1', path: '/mod' + end + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.zip' do + context 'with the root module v1.0.1' do + it_behaves_like 'a module archive resource', 'v1.0.1', ['README.md', 'go.mod', 'a.go'] + end + + context 'with the root module v1.0.2' do + it_behaves_like 'a module archive resource', 'v1.0.2', ['README.md', 'go.mod', 'a.go', 'pkg/b.go'] + end + + context 'with the root module v1.0.3' do + it_behaves_like 'a module archive resource', 'v1.0.3', ['README.md', 'go.mod', 'a.go', 'pkg/b.go'] + end + + context 'with the submodule v1.0.3' do + it_behaves_like 'a module archive resource', 'v1.0.3', ['go.mod', 'a.go'], path: '/mod' + end + + context 'with the root module v2.0.0' do + it_behaves_like 'a module archive resource', 'v2.0.0', ['go.mod', 'a.go', 'x.go'], path: '/v2' + end + end + + context 'with an invalid module directive' do + let_it_be(:project) { create :project_empty_repo, :public, creator: user } + let_it_be(:base) { "#{Settings.build_gitlab_go_url}/#{project.full_path}" } + + let_it_be(:modules) do + create(:go_module_commit, :files, project: project, files: { 'a.go' => "package\a" } ) + create(:go_module_commit, :files, project: project, tag: 'v1.0.0', files: { 'go.mod' => "module not/a/real/module\n" }) + create(:go_module_commit, :files, project: project, files: { 'v2/a.go' => "package a\n" } ) + create(:go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/go.mod' => "module #{base}\n" } ) + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/list' do + context 'with a completely wrong directive for v1' do + it_behaves_like 'a module version list resource' + end + + context 'with a directive omitting the suffix for v2' do + it_behaves_like 'a module version list resource', path: '/v2' + end + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do + context 'with a completely wrong directive for v1' do + it_behaves_like 'a missing module version information resource', 'v1.0.0' + end + + context 'with a directive omitting the suffix for v2' do + it_behaves_like 'a missing module version information resource', 'v2.0.0', path: '/v2' + end + end + end + + context 'with a case sensitive project and versions' do + let_it_be(:project) { create :project_empty_repo, :public, creator: user, path: 'MyGoLib' } + let_it_be(:base) { "#{Settings.build_gitlab_go_url}/#{project.full_path}" } + let_it_be(:base_encoded) { base.gsub(/[A-Z]/) { |s| "!#{s.downcase}"} } + + let_it_be(:modules) do + create(:go_module_commit, :files, project: project, files: { 'README.md' => "Hi" }) + create(:go_module_commit, :module, project: project, tag: 'v1.0.1-prerelease') + create(:go_module_commit, :package, project: project, tag: 'v1.0.1-Prerelease', path: 'pkg') + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/list' do + let(:resource) { "list" } + + context 'with a case encoded path' do + it_behaves_like 'a module version list resource', 'v1.0.1-prerelease', 'v1.0.1-Prerelease' do + let(:module_name) { base_encoded } + end + end + + context 'without a case encoded path' do + it_behaves_like 'a missing module version list resource' do + let(:module_name) { base.downcase } + end + end + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/:module_version.info' do + context 'with a case encoded path' do + it_behaves_like 'a module version information resource', 'v1.0.1-Prerelease' do + let(:module_name) { base_encoded } + let(:resource) { "v1.0.1-!prerelease.info" } + end + end + + context 'without a case encoded path' do + it_behaves_like 'a module version information resource', 'v1.0.1-prerelease' do + let(:module_name) { base_encoded } + let(:resource) { "v1.0.1-prerelease.info" } + end + end + end + end + + context 'with a private project' do + let(:module_name) { base } + + before do + project.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/list' do + let(:resource) { "list" } + + it 'returns ok with an oauth token' do + get_resource(oauth_access_token: oauth) + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'returns ok with a job token' do + get_resource(oauth_access_token: job) + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'returns ok with a personal access token' do + get_resource(personal_access_token: pa_token) + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'returns ok with a personal access token and basic authentication' do + get_resource(headers: build_basic_auth_header(user.username, pa_token.token)) + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'returns unauthorized with no authentication' do + get_resource + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + context 'with a public project' do + let(:module_name) { base } + + before do + project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/list' do + let(:resource) { "list" } + + it 'returns ok with no authentication' do + get_resource + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'with a non-existent project' do + def get_resource(user = nil, **params) + get api("/projects/not%2fa%2fproject/packages/go/#{base}/@v/list", user, params) + end + + describe 'GET /projects/:id/packages/go/*module_name/@v/list' do + it 'returns not found with a user' do + get_resource(user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns not found with an oauth token' do + get_resource(oauth_access_token: oauth) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns not found with a job token' do + get_resource(oauth_access_token: job) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns not found with a personal access token' do + get_resource(personal_access_token: pa_token) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns unauthorized with no authentication' do + get_resource + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + def get_resource(user = nil, headers: {}, **params) + get api("/projects/#{project.id}/packages/go/#{module_name}/@v/#{resource}", user, params), headers: headers + end + + def fmt_pseudo_version(prefix, commit) + "#{prefix}#{commit.committed_date.strftime('%Y%m%d%H%M%S')}-#{commit.sha[0..11]}" + end +end diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb index 3cc1468be02..8a89590c85a 100644 --- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'get board lists' do +RSpec.describe 'get board lists' do include GraphqlHelpers let_it_be(:user) { create(:user) } diff --git a/spec/requests/api/graphql/boards/boards_query_spec.rb b/spec/requests/api/graphql/boards/boards_query_spec.rb index a17554aba21..50004e5a8a1 100644 --- a/spec/requests/api/graphql/boards/boards_query_spec.rb +++ b/spec/requests/api/graphql/boards/boards_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'get list of boards' do +RSpec.describe 'get list of boards' do include GraphqlHelpers include_context 'group and project boards query context' 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 321e1062a96..e298de0df01 100644 --- a/spec/requests/api/graphql/current_user/todos_query_spec.rb +++ b/spec/requests/api/graphql/current_user/todos_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Query current user todos' do +RSpec.describe 'Query current user todos' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/current_user_query_spec.rb b/spec/requests/api/graphql/current_user_query_spec.rb index 2b38b8e98ab..dc832b42fa5 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' -describe 'getting project information' do +RSpec.describe 'getting project information' do include GraphqlHelpers let(:query) do diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb index 266c98d6f08..ee7dba545be 100644 --- a/spec/requests/api/graphql/gitlab_schema_spec.rb +++ b/spec/requests/api/graphql/gitlab_schema_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'GitlabSchema configurations' do +RSpec.describe 'GitlabSchema configurations' do include GraphqlHelpers let_it_be(:project) { create(:project) } diff --git a/spec/requests/api/graphql/group/labels_query_spec.rb b/spec/requests/api/graphql/group/labels_query_spec.rb index 6c34cbadf95..31556ffca30 100644 --- a/spec/requests/api/graphql/group/labels_query_spec.rb +++ b/spec/requests/api/graphql/group/labels_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'getting group label information' do +RSpec.describe 'getting group label information' do include GraphqlHelpers let_it_be(:group) { create(:group, :public) } diff --git a/spec/requests/api/graphql/group/milestones_spec.rb b/spec/requests/api/graphql/group/milestones_spec.rb index bad0024e7a3..380eaea17f8 100644 --- a/spec/requests/api/graphql/group/milestones_spec.rb +++ b/spec/requests/api/graphql/group/milestones_spec.rb @@ -2,21 +2,22 @@ require 'spec_helper' -describe 'Milestones through GroupQuery' do +RSpec.describe 'Milestones through GroupQuery' do include GraphqlHelpers let_it_be(:user) { create(:user) } let_it_be(:now) { Time.now } - let_it_be(:group) { create(:group) } - let_it_be(:milestone_1) { create(:milestone, group: group) } - let_it_be(:milestone_2) { create(:milestone, group: group, state: :closed, start_date: now, due_date: now + 1.day) } - let_it_be(:milestone_3) { create(:milestone, group: group, start_date: now, due_date: now + 2.days) } - let_it_be(:milestone_4) { create(:milestone, group: group, state: :closed, start_date: now - 2.days, due_date: now - 1.day) } - let_it_be(:milestone_from_other_group) { create(:milestone, group: create(:group)) } - - let(:milestone_data) { graphql_data['group']['milestones']['edges'] } describe 'Get list of milestones from a group' do + let_it_be(:group) { create(:group) } + let_it_be(:milestone_1) { create(:milestone, group: group) } + let_it_be(:milestone_2) { create(:milestone, group: group, state: :closed, start_date: now, due_date: now + 1.day) } + let_it_be(:milestone_3) { create(:milestone, group: group, start_date: now, due_date: now + 2.days) } + let_it_be(:milestone_4) { create(:milestone, group: group, state: :closed, start_date: now - 2.days, due_date: now - 1.day) } + let_it_be(:milestone_from_other_group) { create(:milestone, group: create(:group)) } + + let(:milestone_data) { graphql_data['group']['milestones']['edges'] } + context 'when the request is correct' do before do fetch_milestones(user) @@ -72,21 +73,6 @@ describe 'Milestones through GroupQuery' do submilestone_1.to_global_id.to_s, submilestone_2.to_global_id.to_s ) end - - context 'when group_milestone_descendants is disabled' do - before do - stub_feature_flags(group_milestone_descendants: false) - end - - it 'ignores descendant milestones' do - fetch_milestones(user, args) - - expect_array_response( - milestone_1.to_global_id.to_s, milestone_2.to_global_id.to_s, - milestone_3.to_global_id.to_s, milestone_4.to_global_id.to_s - ) - end - end end def fetch_milestones(user = nil, args = {}) @@ -120,4 +106,89 @@ describe 'Milestones through GroupQuery' do node_array(milestone_data, extract_attribute) end end + + describe 'ensures each field returns the correct value' do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:milestone) { create(:milestone, group: group, start_date: now, due_date: now + 1.day) } + let_it_be(:open_issue) { create(:issue, project: project, milestone: milestone) } + let_it_be(:closed_issue) { create(:issue, :closed, project: project, milestone: milestone) } + + let(:milestone_query) do + %{ + id + title + description + state + webPath + dueDate + startDate + createdAt + updatedAt + projectMilestone + groupMilestone + subgroupMilestone + } + end + + def post_query + full_query = graphql_query_for("group", + { full_path: group.full_path }, + [query_graphql_field("milestones", nil, "nodes { #{milestone_query} }")] + ) + + post_graphql(full_query, current_user: user) + + graphql_data.dig('group', 'milestones', 'nodes', 0) + end + + it 'returns correct values for scalar fields' do + expect(post_query).to eq({ + 'id' => global_id_of(milestone), + 'title' => milestone.title, + 'description' => milestone.description, + 'state' => 'active', + 'webPath' => milestone_path(milestone), + 'dueDate' => milestone.due_date.iso8601, + 'startDate' => milestone.start_date.iso8601, + 'createdAt' => milestone.created_at.iso8601, + 'updatedAt' => milestone.updated_at.iso8601, + 'projectMilestone' => false, + 'groupMilestone' => true, + 'subgroupMilestone' => false + }) + end + + context 'milestone statistics' do + let(:milestone_query) do + %{ + stats { + totalIssuesCount + closedIssuesCount + } + } + end + + it 'returns the correct milestone statistics' do + expect(post_query).to eq({ + 'stats' => { + 'totalIssuesCount' => 2, + 'closedIssuesCount' => 1 + } + }) + end + + context 'when the graphql_milestone_stats feature flag is disabled' do + before do + stub_feature_flags(graphql_milestone_stats: false) + end + + it 'returns nil for the stats field' do + expect(post_query).to eq({ + 'stats' => nil + }) + end + end + end + end end diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb index c7b537a9923..d99bff2e349 100644 --- a/spec/requests/api/graphql/group_query_spec.rb +++ b/spec/requests/api/graphql/group_query_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' # Based on spec/requests/api/groups_spec.rb # Should follow closely in order to ensure all situations are covered -describe 'getting group information', :do_not_mock_admin_mode do +RSpec.describe 'getting group information', :do_not_mock_admin_mode do include GraphqlHelpers include UploadHelpers diff --git a/spec/requests/api/graphql/metadata_query_spec.rb b/spec/requests/api/graphql/metadata_query_spec.rb index 4c56c559cf9..6344ec371c8 100644 --- a/spec/requests/api/graphql/metadata_query_spec.rb +++ b/spec/requests/api/graphql/metadata_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'getting project information' do +RSpec.describe 'getting project information' do include GraphqlHelpers let(:query) { graphql_query_for('metadata', {}, all_graphql_fields_for('Metadata')) } diff --git a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb index cb35411b7a5..c47920087dc 100644 --- a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb +++ b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Getting Metrics Dashboard Annotations' do +RSpec.describe 'Getting Metrics Dashboard Annotations' do include GraphqlHelpers let_it_be(:project) { create(:project) } diff --git a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb index d9d9ea9ad61..456b0a5dea1 100644 --- a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb +++ b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Getting Metrics Dashboard' do +RSpec.describe 'Getting Metrics Dashboard' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } @@ -62,12 +62,12 @@ describe 'Getting Metrics Dashboard' do context 'invalid dashboard' do let(:path) { '.gitlab/dashboards/metrics.yml' } - let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndasboard: ''" }) } + let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndashboard: 'test'" }) } it 'returns metrics dashboard' do dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') - expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: can't be blank"]) + expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["panel_groups: should be an array of panel_groups objects"]) end end @@ -78,7 +78,7 @@ describe 'Getting Metrics Dashboard' do it 'returns metrics dashboard' do dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard') - expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: can't be blank"]) + expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: should be an array of panel_groups objects"]) end end end diff --git a/spec/requests/api/graphql/multiplexed_queries_spec.rb b/spec/requests/api/graphql/multiplexed_queries_spec.rb index 9ebb57f6b9c..f79bac6ae3b 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' -describe 'Multiplexed queries' do +RSpec.describe 'Multiplexed queries' do include GraphqlHelpers it 'returns responses for multiple queries' do 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 a5159da84f3..4ad35e7f0d1 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' -describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues do +RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues do include GraphqlHelpers let_it_be(:admin) { create(:admin) } diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb index 5b5b2ec8788..6141a172253 100644 --- a/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb +++ b/spec/requests/api/graphql/mutations/alert_management/alerts/create_alert_issue_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Create an alert issue from an alert' do +RSpec.describe 'Create an alert issue from an alert' do include GraphqlHelpers let_it_be(:user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/set_assignees_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/set_assignees_spec.rb index 6663281e093..cd5cefa0a9a 100644 --- a/spec/requests/api/graphql/mutations/alert_management/alerts/set_assignees_spec.rb +++ b/spec/requests/api/graphql/mutations/alert_management/alerts/set_assignees_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Setting assignees of an alert' do +RSpec.describe 'Setting assignees of an alert' do include GraphqlHelpers let_it_be(:project) { create(:project) } diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/todo/create_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/todo/create_spec.rb new file mode 100644 index 00000000000..e5803f50474 --- /dev/null +++ b/spec/requests/api/graphql/mutations/alert_management/alerts/todo/create_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Creating a todo for the alert' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let(:alert) { create(:alert_management_alert, project: project) } + + let(:mutation) do + variables = { + project_path: project.full_path, + iid: alert.iid.to_s + } + graphql_mutation(:alert_todo_create, variables) do + <<~QL + clientMutationId + errors + todo { + author { + username + } + } + QL + end + end + + let(:mutation_response) { graphql_mutation_response(:alert_todo_create) } + + before do + project.add_developer(user) + end + + it 'creates a todo for the current user' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['todo']['author']['username']).to eq(user.username) + end + + context 'todo already exists' do + before do + create(:todo, :pending, project: project, user: user, target: alert) + end + + it 'surfaces an error' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to eq(['You already have pending todo for this alert']) + end + end +end diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb index 2a470bda689..ff55656a2ae 100644 --- a/spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb +++ b/spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Setting the status of an alert' do +RSpec.describe 'Setting the status of an alert' do include GraphqlHelpers let_it_be(:user) { create(:user) } 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 83dec7dd3e2..1891300dace 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' -describe 'Adding an AwardEmoji' do +RSpec.describe 'Adding an AwardEmoji' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } @@ -15,11 +15,11 @@ describe 'Adding an AwardEmoji' do name: emoji_name } - graphql_mutation(:add_award_emoji, variables) + graphql_mutation(:award_emoji_add, variables) end def mutation_response - graphql_mutation_response(:add_award_emoji) + graphql_mutation_response(:award_emoji_add) end shared_examples 'a mutation that does not create an AwardEmoji' do 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 a2997db6cae..665b511abb8 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' -describe 'Removing an AwardEmoji' do +RSpec.describe 'Removing an AwardEmoji' do include GraphqlHelpers let(:current_user) { create(:user) } @@ -12,11 +12,11 @@ describe 'Removing an AwardEmoji' do let(:input) { { awardable_id: GitlabSchema.id_from_object(awardable).to_s, name: emoji_name } } let(:mutation) do - graphql_mutation(:remove_award_emoji, input) + graphql_mutation(:award_emoji_remove, input) end def mutation_response - graphql_mutation_response(:remove_award_emoji) + graphql_mutation_response(:award_emoji_remove) end def create_award_emoji(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 e1180c85c6b..ab4a213fde3 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' -describe 'Toggling an AwardEmoji' do +RSpec.describe 'Toggling an AwardEmoji' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } @@ -15,11 +15,11 @@ describe 'Toggling an AwardEmoji' do name: emoji_name } - graphql_mutation(:toggle_award_emoji, variables) + graphql_mutation(:award_emoji_toggle, variables) end def mutation_response - graphql_mutation_response(:toggle_award_emoji) + graphql_mutation_response(:award_emoji_toggle) end shared_examples 'a mutation that does not create or destroy an AwardEmoji' do diff --git a/spec/requests/api/graphql/mutations/branches/create_spec.rb b/spec/requests/api/graphql/mutations/branches/create_spec.rb index b3c378ec2bc..082b445bf3e 100644 --- a/spec/requests/api/graphql/mutations/branches/create_spec.rb +++ b/spec/requests/api/graphql/mutations/branches/create_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Creation of a new branch' do +RSpec.describe 'Creation of a new branch' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/commits/create_spec.rb b/spec/requests/api/graphql/mutations/commits/create_spec.rb index 10a69932948..9e4a96700bb 100644 --- a/spec/requests/api/graphql/mutations/commits/create_spec.rb +++ b/spec/requests/api/graphql/mutations/commits/create_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Creation of a new commit' do +RSpec.describe 'Creation of a new commit' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb b/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb index bc256a08f00..bc1b42d68e6 100644 --- a/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb +++ b/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Updating the container expiration policy' do +RSpec.describe 'Updating the container expiration policy' do include GraphqlHelpers using RSpec::Parameterized::TableSyntax @@ -48,13 +48,48 @@ describe 'Updating the container expiration policy' do end end - RSpec.shared_examples 'updating the container expiration policy' do + RSpec.shared_examples 'rejecting invalid regex for' do |field_name| + context "for field #{field_name}" do + let_it_be(:invalid_regex) { '*production' } + let(:params) do + { + :project_path => project.full_path, + field_name => invalid_regex + } + end + + it_behaves_like 'returning response status', :success + + it_behaves_like 'not creating the container expiration policy' + + it 'returns an error' do + subject + + expect(graphql_errors.size).to eq(1) + expect(graphql_errors.first['message']).to include("#{invalid_regex} is an invalid regexp") + end + end + end + + RSpec.shared_examples 'accepting the mutation request updating the container expiration policy' do it_behaves_like 'updating the container expiration policy attributes', mode: :update, from: { cadence: '1d', keep_n: 10, older_than: '90d' }, to: { cadence: '3month', keep_n: 100, older_than: '14d' } it_behaves_like 'returning a success' + + it_behaves_like 'rejecting invalid regex for', :name_regex + it_behaves_like 'rejecting invalid regex for', :name_regex_keep + end + + RSpec.shared_examples 'accepting the mutation request creating the container expiration policy' do + it_behaves_like 'creating the container expiration policy' + + it_behaves_like 'returning a success' + + it_behaves_like 'rejecting invalid regex for', :name_regex + it_behaves_like 'rejecting invalid regex for', :name_regex_keep end - RSpec.shared_examples 'denying access to container expiration policy' do + RSpec.shared_examples 'denying the mutation request' do it_behaves_like 'not creating the container expiration policy' it_behaves_like 'returning response status', :success @@ -71,11 +106,11 @@ describe 'Updating the container expiration policy' do context 'with existing container expiration policy' do where(:user_role, :shared_examples_name) do - :maintainer | 'updating the container expiration policy' - :developer | 'updating the container expiration policy' - :reporter | 'denying access to container expiration policy' - :guest | 'denying access to container expiration policy' - :anonymous | 'denying access to container expiration policy' + :maintainer | 'accepting the mutation request updating the container expiration policy' + :developer | 'accepting the mutation request updating the container expiration policy' + :reporter | 'denying the mutation request' + :guest | 'denying the mutation request' + :anonymous | 'denying the mutation request' end with_them do @@ -91,11 +126,11 @@ describe 'Updating the container expiration policy' do let_it_be(:project, reload: true) { create(:project, :without_container_expiration_policy) } where(:user_role, :shared_examples_name) do - :maintainer | 'creating the container expiration policy' - :developer | 'creating the container expiration policy' - :reporter | 'denying access to container expiration policy' - :guest | 'denying access to container expiration policy' - :anonymous | 'denying access to container expiration policy' + :maintainer | 'accepting the mutation request creating the container expiration policy' + :developer | 'accepting the mutation request creating the container expiration policy' + :reporter | 'denying the mutation request' + :guest | 'denying the mutation request' + :anonymous | 'denying the mutation request' end with_them do diff --git a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb index 10376305b3e..e329416faee 100644 --- a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb +++ b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe "deleting designs" do +RSpec.describe "deleting designs" do include GraphqlHelpers include DesignManagementTestHelpers diff --git a/spec/requests/api/graphql/mutations/design_management/upload_spec.rb b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb index 22adc064406..9a9c7107b20 100644 --- a/spec/requests/api/graphql/mutations/design_management/upload_spec.rb +++ b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "spec_helper" -describe "uploading designs" do +RSpec.describe "uploading designs" do include GraphqlHelpers include DesignManagementTestHelpers include WorkhorseHelpers diff --git a/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb b/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb index 95e967c039d..e83da830935 100644 --- a/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb +++ b/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Toggling the resolve status of a discussion' do +RSpec.describe 'Toggling the resolve status of a discussion' do include GraphqlHelpers let_it_be(:project) { create(:project, :public, :repository) } diff --git a/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb b/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb index 4d0bb59b030..3f804a46992 100644 --- a/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/set_confidential_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Setting an issue as confidential' do +RSpec.describe 'Setting an issue as confidential' do include GraphqlHelpers let(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb b/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb index 1efa9e16233..3dd1225db5a 100644 --- a/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/set_due_date_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Setting Due Date of an issue' do +RSpec.describe 'Setting Due Date of an issue' do include GraphqlHelpers let(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb b/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb new file mode 100644 index 00000000000..f1d55430e02 --- /dev/null +++ b/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Setting an issue as locked' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:issue) { create(:issue) } + let_it_be(:project) { issue.project } + let(:input) { { locked: true } } + + let(:mutation) do + variables = { + project_path: project.full_path, + iid: issue.iid.to_s + } + graphql_mutation(:issue_set_locked, variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + issue { + iid + discussionLocked + } + QL + ) + end + + def mutation_response + graphql_mutation_response(:issue_set_locked) + end + + context 'when the user is not allowed to update the issue' do + it 'returns an error' do + error = "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_errors).to include(a_hash_including('message' => error)) + end + end + + context 'when user is allowed to update the issue' do + before do + project.add_developer(current_user) + end + + it 'updates the issue locked status' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['issue']['discussionLocked']).to be_truthy + end + end +end diff --git a/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb b/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb index be0d843d5ff..4057aa4ba9e 100644 --- a/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb +++ b/spec/requests/api/graphql/mutations/jira_import/import_users_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Importing Jira Users' do +RSpec.describe 'Importing Jira Users' do include JiraServiceHelper include GraphqlHelpers diff --git a/spec/requests/api/graphql/mutations/jira_import/start_spec.rb b/spec/requests/api/graphql/mutations/jira_import/start_spec.rb index 296d33aec5d..e7124512ef1 100644 --- a/spec/requests/api/graphql/mutations/jira_import/start_spec.rb +++ b/spec/requests/api/graphql/mutations/jira_import/start_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Starting a Jira Import' do +RSpec.describe 'Starting a Jira Import' do include JiraServiceHelper include GraphqlHelpers @@ -14,7 +14,8 @@ describe 'Starting a Jira Import' do let(:mutation) do variables = { jira_project_key: jira_project_key, - project_path: project_path + project_path: project_path, + users_mapping: [{ jiraAccountId: 'abc', gitlabId: 5 }] } graphql_mutation(:jira_import_start, variables) diff --git a/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb index 5c63f655f1d..d4ac639e226 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Creation of a new merge request' do +RSpec.describe 'Creation of a new merge request' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } 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 8f908b7bf88..97873b01338 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 @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Setting assignees of a merge request' do +RSpec.describe 'Setting assignees of a merge request' do include GraphqlHelpers let(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb index 2112ff0dc74..34d347c76fd 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Setting labels of a merge request' do +RSpec.describe 'Setting labels of a merge request' do include GraphqlHelpers let(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb index c45da613591..a1a35bc1dcc 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Setting locked status of a merge request' do +RSpec.describe 'Setting locked status of a merge request' do include GraphqlHelpers let(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb index bd558edf9c5..d7e2602bd0a 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Setting milestone of a merge request' do +RSpec.describe 'Setting milestone of a merge request' do include GraphqlHelpers let(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_subscription_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_subscription_spec.rb index 975735bf246..6b3035fbf48 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_subscription_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_subscription_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Setting subscribed status of a merge request' do +RSpec.describe 'Setting subscribed status of a merge request' do include GraphqlHelpers let(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb index 4492c51dbd7..2143abd3031 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Setting WIP status of a merge request' do +RSpec.describe 'Setting Draft status of a merge request' do include GraphqlHelpers let(:current_user) { create(:user) } @@ -41,39 +41,39 @@ describe 'Setting WIP status of a merge request' do expect(graphql_errors).not_to be_empty end - it 'marks the merge request as WIP' do + it 'marks the merge request as Draft' do post_graphql_mutation(mutation, current_user: current_user) expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['mergeRequest']['title']).to start_with('WIP:') + expect(mutation_response['mergeRequest']['title']).to start_with('Draft:') end - it 'does not do anything if the merge request was already marked `WIP`' do - merge_request.update!(title: 'wip: hello world') + it 'does not do anything if the merge request was already marked `Draft`' do + merge_request.update!(title: 'draft: hello world') post_graphql_mutation(mutation, current_user: current_user) expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['mergeRequest']['title']).to start_with('wip:') + expect(mutation_response['mergeRequest']['title']).to start_with('draft:') end - context 'when passing WIP false as input' do + context 'when passing Draft false as input' do let(:input) { { wip: false } } - it 'does not do anything if the merge reqeust was not marked wip' do + it 'does not do anything if the merge reqeust was not marked draft' do post_graphql_mutation(mutation, current_user: current_user) expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['mergeRequest']['title']).not_to start_with(/wip\:/) + expect(mutation_response['mergeRequest']['title']).not_to start_with(/draft\:/) end - it 'unmarks the merge request as `WIP`' do - merge_request.update!(title: 'wip: hello world') + it 'unmarks the merge request as `Draft`' do + merge_request.update!(title: 'draft: hello world') post_graphql_mutation(mutation, current_user: current_user) expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['mergeRequest']['title']).not_to start_with('/wip\:/') + expect(mutation_response['mergeRequest']['title']).not_to start_with('/draft\:/') end end end 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 8568dc8ffc0..0e2da94f0f9 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 @@ -2,7 +2,7 @@ require 'spec_helper' -describe Mutations::Metrics::Dashboard::Annotations::Create do +RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do include GraphqlHelpers let_it_be(:current_user) { create(:user) } 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 217f538c53e..2459a6f3828 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 @@ -2,7 +2,7 @@ require 'spec_helper' -describe Mutations::Metrics::Dashboard::Annotations::Delete do +RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb index 4c535434faa..e847c46be1b 100644 --- a/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Adding a DiffNote' do +RSpec.describe 'Adding a DiffNote' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb index 0bba3e79434..896a398e308 100644 --- a/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Adding an image DiffNote' do +RSpec.describe 'Adding an image DiffNote' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } 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 9a78d44245e..391ced7dc98 100644 --- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Adding a Note' do +RSpec.describe 'Adding a Note' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } @@ -13,7 +13,8 @@ describe 'Adding a Note' do variables = { noteable_id: GitlabSchema.id_from_object(noteable).to_s, discussion_id: (GitlabSchema.id_from_object(discussion).to_s if discussion), - body: 'Body text' + body: 'Body text', + confidential: true } graphql_mutation(:create_note, variables) @@ -40,6 +41,7 @@ describe 'Adding a Note' do post_graphql_mutation(mutation, current_user: current_user) expect(mutation_response['note']['body']).to eq('Body text') + expect(mutation_response['note']['confidential']).to eq(true) end describe 'creating Notes in reply to a discussion' do diff --git a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb index 337a6e6f6e6..6002a5b5b9d 100644 --- a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Destroying a Note' do +RSpec.describe 'Destroying a Note' do include GraphqlHelpers let!(:note) { create(:note) } diff --git a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb index 0362fef2d2e..f7be671e5f3 100644 --- a/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Updating an image DiffNote' do +RSpec.describe 'Updating an image DiffNote' do include GraphqlHelpers using RSpec::Parameterized::TableSyntax diff --git a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb index a5c6b72005e..38378310d9f 100644 --- a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Updating a Note' do +RSpec.describe 'Updating a Note' do include GraphqlHelpers let!(:note) { create(:note, note: original_body) } diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb index 9052f54b171..e2474e1bcce 100644 --- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Creating a Snippet' do +RSpec.describe 'Creating a Snippet' do include GraphqlHelpers let_it_be(:user) { create(:user) } @@ -14,9 +14,8 @@ describe 'Creating a Snippet' do let(:visibility_level) { 'public' } let(:project_path) { nil } let(:uploaded_files) { nil } - - let(:mutation) do - variables = { + let(:mutation_vars) do + { content: content, description: description, visibility_level: visibility_level, @@ -25,8 +24,10 @@ describe 'Creating a Snippet' do project_path: project_path, uploaded_files: uploaded_files } + end - graphql_mutation(:create_snippet, variables) + let(:mutation) do + graphql_mutation(:create_snippet, mutation_vars) end def mutation_response @@ -137,6 +138,47 @@ describe 'Creating a Snippet' do end end + context 'when snippet is created using the files param' do + let(:action) { :create } + let(:file_1) { { filePath: 'example_file1', content: 'This is the example file 1' }} + let(:file_2) { { filePath: 'example_file2', content: 'This is the example file 2' }} + let(:actions) { [{ action: action }.merge(file_1), { action: action }.merge(file_2)] } + let(:mutation_vars) do + { + description: description, + visibility_level: visibility_level, + project_path: project_path, + title: title, + files: actions + } + end + + it 'creates the Snippet' do + expect do + subject + end.to change { Snippet.count }.by(1) + end + + it 'returns the created Snippet' do + subject + + expect(mutation_response['snippet']['title']).to eq(title) + expect(mutation_response['snippet']['description']).to eq(description) + expect(mutation_response['snippet']['visibilityLevel']).to eq(visibility_level) + expect(mutation_response['snippet']['blobs'][0]['plainData']).to match(file_1[:content]) + expect(mutation_response['snippet']['blobs'][0]['fileName']).to match(file_1[:file_path]) + expect(mutation_response['snippet']['blobs'][1]['plainData']).to match(file_2[:content]) + expect(mutation_response['snippet']['blobs'][1]['fileName']).to match(file_2[:file_path]) + end + + context 'when action is invalid' do + let(:file_1) { { filePath: 'example_file1' }} + + it_behaves_like 'a mutation that returns errors in the response', errors: ['Snippet actions have invalid data'] + it_behaves_like 'does not create snippet' + end + end + context 'when there are ActiveRecord validation errors' do let(:title) { '' } diff --git a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb index cb9aeea74b2..8ade72635af 100644 --- a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Destroying a Snippet' do +RSpec.describe 'Destroying a Snippet' do include GraphqlHelpers let(:current_user) { snippet.author } diff --git a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb index 6d4dce3f6f1..97e6ae8fda8 100644 --- a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Mark snippet as spam', :do_not_mock_admin_mode do +RSpec.describe 'Mark snippet as spam', :do_not_mock_admin_mode do include GraphqlHelpers let_it_be(:admin) { create(:admin) } diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb index 968ea5aed52..3b2f9dc0f19 100644 --- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Updating a Snippet' do +RSpec.describe 'Updating a Snippet' do include GraphqlHelpers let_it_be(:original_content) { 'Initial content' } @@ -16,8 +16,8 @@ describe 'Updating a Snippet' do let(:current_user) { snippet.author } let(:snippet_gid) { GitlabSchema.id_from_object(snippet).to_s } - let(:mutation) do - variables = { + let(:mutation_vars) do + { id: snippet_gid, content: updated_content, description: updated_description, @@ -25,8 +25,9 @@ describe 'Updating a Snippet' do file_name: updated_file_name, title: updated_title } - - graphql_mutation(:update_snippet, variables) + end + let(:mutation) do + graphql_mutation(:update_snippet, mutation_vars) end def mutation_response @@ -101,7 +102,6 @@ describe 'Updating a Snippet' do end it_behaves_like 'graphql update actions' - it_behaves_like 'when the snippet is not found' end @@ -148,4 +148,40 @@ describe 'Updating a Snippet' do it_behaves_like 'when the snippet is not found' end + + context 'when using the files params' do + let!(:snippet) { create(:personal_snippet, :private, :repository) } + let(:updated_content) { 'updated_content' } + let(:updated_file) { 'CHANGELOG' } + let(:deleted_file) { 'README' } + let(:mutation_vars) do + { + id: snippet_gid, + files: [ + { action: :update, filePath: updated_file, content: updated_content }, + { action: :delete, filePath: deleted_file } + ] + } + end + + it 'updates the Snippet' do + blob_to_update = blob_at(updated_file) + expect(blob_to_update.data).not_to eq updated_content + + blob_to_delete = blob_at(deleted_file) + expect(blob_to_delete).to be_present + + post_graphql_mutation(mutation, current_user: current_user) + + blob_to_update = blob_at(updated_file) + expect(blob_to_update.data).to eq updated_content + + blob_to_delete = blob_at(deleted_file) + expect(blob_to_delete).to be_nil + end + + def blob_at(filename) + snippet.repository.blob_at('HEAD', filename) + end + end end diff --git a/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb b/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb index 40e085027d7..ed5552f3e30 100644 --- a/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Marking all todos done' do +RSpec.describe 'Marking all todos done' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb index fabbb3aeb49..9c4733f6769 100644 --- a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Marking todos done' do +RSpec.describe 'Marking todos done' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/mutations/todos/restore_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_spec.rb index faa36c8273a..6dedde56e13 100644 --- a/spec/requests/api/graphql/mutations/todos/restore_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/restore_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Restoring Todos' do +RSpec.describe 'Restoring Todos' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb index 2a95b99572f..0b634e6b689 100644 --- a/spec/requests/api/graphql/namespace/projects_spec.rb +++ b/spec/requests/api/graphql/namespace/projects_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'getting projects' do +RSpec.describe 'getting projects' do include GraphqlHelpers let(:group) { create(:group) } diff --git a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb index f7e28043930..44e68c59248 100644 --- a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb +++ b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'rendering namespace statistics' do +RSpec.describe 'rendering namespace statistics' do include GraphqlHelpers let(:namespace) { user.namespace } diff --git a/spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb index 4c048caaeee..dd001a73349 100644 --- a/spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'getting Alert Management Alert Assignees' do +RSpec.describe 'getting Alert Management Alert Assignees' do include GraphqlHelpers let_it_be(:project) { create(:project) } 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 new file mode 100644 index 00000000000..352a94cfc1d --- /dev/null +++ b/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting Alert Management Alert Assignees' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:current_user) { create(:user) } + + let(:fields) do + <<~QUERY + nodes { + iid + metricsDashboardUrl + } + QUERY + end + + let(:graphql_query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('alertManagementAlerts', {}, fields) + ) + end + + let(:alerts) { graphql_data.dig('project', 'alertManagementAlerts', 'nodes') } + let(:first_alert) { alerts.first } + + before do + project.add_developer(current_user) + end + + context 'with self-managed prometheus payload' do + include_context 'self-managed prometheus alert attributes' + + before do + create(:alert_management_alert, :prometheus, project: project, payload: payload) + end + + it 'includes the correct metrics dashboard url' do + post_graphql(graphql_query, current_user: current_user) + + expect(first_alert).to include('metricsDashboardUrl' => dashboard_url_for_alert) + end + end + + context 'with gitlab-managed prometheus payload' do + include_context 'gitlab-managed prometheus alert attributes' + + before do + create(:alert_management_alert, :prometheus, project: project, payload: payload, prometheus_alert: prometheus_alert) + end + + it 'includes the correct metrics dashboard url' do + post_graphql(graphql_query, current_user: current_user) + + expect(first_alert).to include('metricsDashboardUrl' => dashboard_url_for_alert) + 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 df6bfa8c97b..1350cba119b 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 @@ -2,15 +2,15 @@ require 'spec_helper' -describe 'getting Alert Management Alert Notes' do +RSpec.describe 'getting Alert Management Alert Notes' do include GraphqlHelpers let_it_be(:project) { create(:project) } let_it_be(:current_user) { create(:user) } let_it_be(:first_alert) { create(:alert_management_alert, project: project, assignees: [current_user]) } let_it_be(:second_alert) { create(:alert_management_alert, project: project) } - let_it_be(:first_system_note) { create(:note_on_alert, noteable: first_alert, project: project) } - let_it_be(:second_system_note) { create(:note_on_alert, noteable: first_alert, project: project) } + let_it_be(:first_system_note) { create(:note_on_alert, :with_system_note_metadata, noteable: first_alert, project: project) } + let_it_be(:second_system_note) { create(:note_on_alert, :with_system_note_metadata, noteable: first_alert, project: project) } let(:params) { {} } @@ -21,6 +21,8 @@ describe 'getting Alert Management Alert Notes' do notes { nodes { id + body + systemNoteIconName } } } @@ -44,7 +46,17 @@ describe 'getting Alert Management Alert Notes' do project.add_developer(current_user) end - it 'returns the notes ordered by createdAt' do + it 'includes expected data' do + post_graphql(query, current_user: current_user) + + expect(first_notes_result.first).to include( + 'id' => first_system_note.to_global_id.to_s, + 'systemNoteIconName' => 'git-merge', + 'body' => first_system_note.note + ) + end + + it 'returns the notes ordered by createdAt with sufficient content' do post_graphql(query, current_user: current_user) expect(first_notes_result.length).to eq(2) @@ -64,4 +76,18 @@ describe 'getting Alert Management Alert Notes' do expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(base_count) expect(alerts_result.length).to eq(3) end + + context 'for non-system notes' do + let_it_be(:user_note) { create(:note_on_alert, noteable: second_alert, project: project) } + + it 'includes expected data' do + post_graphql(query, current_user: current_user) + + expect(second_notes_result.first).to include( + 'id' => user_note.to_global_id.to_s, + 'systemNoteIconName' => nil, + 'body' => user_note.note + ) + end + end end diff --git a/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb b/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb index a0d1ff7efc5..b62215f43fb 100644 --- a/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -describe 'getting Alert Management Alert counts by status' do +RSpec.describe 'getting Alert Management Alert counts by status' do include GraphqlHelpers let_it_be(:project) { create(:project, :repository) } diff --git a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb index c591895f295..f050c6873f3 100644 --- a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -describe 'getting Alert Management Alerts' do +RSpec.describe 'getting Alert Management Alerts' do include GraphqlHelpers let_it_be(:payload) { { 'custom' => { 'alert' => 'payload' } } } @@ -73,12 +73,13 @@ describe 'getting Alert Management Alerts' do 'endedAt' => nil, 'details' => { 'custom.alert' => 'payload' }, 'createdAt' => triggered_alert.created_at.strftime('%Y-%m-%dT%H:%M:%SZ'), - 'updatedAt' => triggered_alert.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ') + 'updatedAt' => triggered_alert.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ'), + 'metricsDashboardUrl' => nil ) expect(second_alert).to include( 'iid' => resolved_alert.iid.to_s, - 'issueIid' => nil, + 'issueIid' => resolved_alert.issue_iid.to_s, 'status' => 'RESOLVED', 'endedAt' => resolved_alert.ended_at.strftime('%Y-%m-%dT%H:%M:%SZ') ) @@ -109,14 +110,14 @@ describe 'getting Alert Management Alerts' do it_behaves_like 'a working graphql query' it 'sorts in the correct order' do - expect(iids).to eq [resolved_alert.iid.to_s, triggered_alert.iid.to_s] + expect(iids).to eq [triggered_alert.iid.to_s, resolved_alert.iid.to_s] end context 'ascending order' do let(:params) { 'sort: SEVERITY_ASC' } it 'sorts in the correct order' do - expect(iids).to eq [triggered_alert.iid.to_s, resolved_alert.iid.to_s] + expect(iids).to eq [resolved_alert.iid.to_s, triggered_alert.iid.to_s] end end end diff --git a/spec/requests/api/graphql/project/base_service_spec.rb b/spec/requests/api/graphql/project/base_service_spec.rb index 8199f331fbf..4dfc242da80 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' -describe 'query Jira service' do +RSpec.describe 'query Jira service' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/project/container_expiration_policy_spec.rb b/spec/requests/api/graphql/project/container_expiration_policy_spec.rb index d0563f9ff05..b064e4d43e9 100644 --- a/spec/requests/api/graphql/project/container_expiration_policy_spec.rb +++ b/spec/requests/api/graphql/project/container_expiration_policy_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -describe 'getting a repository in a project' do +RSpec.describe 'getting a repository in a project' do include GraphqlHelpers let_it_be(:project) { create(:project) } diff --git a/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb b/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb index a1f9fa1f10c..b2b42137acf 100644 --- a/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb +++ b/spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -describe 'getting a detailed sentry error' do +RSpec.describe 'getting a detailed sentry error' do include GraphqlHelpers let_it_be(:project) { create(:project, :repository) } diff --git a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb index 06a0bfc0d32..cd84ce9cb96 100644 --- a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb +++ b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -describe 'sentry errors requests' do +RSpec.describe 'sentry errors requests' do include GraphqlHelpers let_it_be(:project) { create(:project, :repository) } let_it_be(:project_setting) { create(:project_error_tracking_setting, project: project) } diff --git a/spec/requests/api/graphql/project/grafana_integration_spec.rb b/spec/requests/api/graphql/project/grafana_integration_spec.rb index c9bc6c1a68e..688959e622d 100644 --- a/spec/requests/api/graphql/project/grafana_integration_spec.rb +++ b/spec/requests/api/graphql/project/grafana_integration_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -describe 'Getting Grafana Integration' do +RSpec.describe 'Getting Grafana Integration' do include GraphqlHelpers let_it_be(:project) { create(:project, :repository) } diff --git a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb index 04f445b4318..1b654e660e3 100644 --- a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb +++ b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)' do +RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)' do include GraphqlHelpers include DesignManagementTestHelpers diff --git a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb index 18787bf925d..640ac95cd86 100644 --- a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb +++ b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Getting versions related to an issue' do +RSpec.describe 'Getting versions related to an issue' do include GraphqlHelpers include DesignManagementTestHelpers diff --git a/spec/requests/api/graphql/project/issue/designs/designs_spec.rb b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb index b6fd0d91bda..e47c025f8b2 100644 --- a/spec/requests/api/graphql/project/issue/designs/designs_spec.rb +++ b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Getting designs related to an issue' do +RSpec.describe 'Getting designs related to an issue' do include GraphqlHelpers include DesignManagementTestHelpers diff --git a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb index 0207bb9123a..ae5c8363d0f 100644 --- a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb +++ b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Getting designs related to an issue' do +RSpec.describe 'Getting designs related to an issue' do include GraphqlHelpers include DesignManagementTestHelpers diff --git a/spec/requests/api/graphql/project/issue/notes_spec.rb b/spec/requests/api/graphql/project/issue/notes_spec.rb index bfc89434370..97f5261ef1d 100644 --- a/spec/requests/api/graphql/project/issue/notes_spec.rb +++ b/spec/requests/api/graphql/project/issue/notes_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'getting notes for an issue' do +RSpec.describe 'getting notes for an issue' do include GraphqlHelpers let(:noteable) { create(:issue) } diff --git a/spec/requests/api/graphql/project/issue_spec.rb b/spec/requests/api/graphql/project/issue_spec.rb index 92d2f9d0d31..5f368833181 100644 --- a/spec/requests/api/graphql/project/issue_spec.rb +++ b/spec/requests/api/graphql/project/issue_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Query.project(fullPath).issue(iid)' do +RSpec.describe 'Query.project(fullPath).issue(iid)' do include GraphqlHelpers let_it_be(:project) { create(:project) } diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 3128f527356..cdfff2f50d4 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'getting an issue list for a project' do +RSpec.describe 'getting an issue list for a project' do include GraphqlHelpers let(:project) { create(:project, :repository, :public) } diff --git a/spec/requests/api/graphql/project/jira_import_spec.rb b/spec/requests/api/graphql/project/jira_import_spec.rb index 7be14696963..814965262b6 100644 --- a/spec/requests/api/graphql/project/jira_import_spec.rb +++ b/spec/requests/api/graphql/project/jira_import_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'query Jira import data' do +RSpec.describe 'query Jira import data' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/project/jira_projects_spec.rb b/spec/requests/api/graphql/project/jira_projects_spec.rb index d67c89f18c9..d5f59711ab1 100644 --- a/spec/requests/api/graphql/project/jira_projects_spec.rb +++ b/spec/requests/api/graphql/project/jira_projects_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'query Jira projects' do +RSpec.describe 'query Jira projects' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } @@ -80,34 +80,6 @@ describe 'query Jira projects' do it_behaves_like 'fetches first project' end - - context 'with before cursor' do - let(:projects_query) { 'projects(before: "Mg==", first: 1)' } - - it_behaves_like 'fetches first project' - end - - context 'with after cursor' do - let(:projects_query) { 'projects(after: "MA==", first: 1)' } - - it_behaves_like 'fetches first project' - end - end - - context 'with valid but inexistent after cursor' do - let(:projects_query) { 'projects(after: "MTk==")' } - - it 'retuns empty list of jira projects' do - expect(jira_projects.size).to eq(0) - end - end - - context 'with invalid after cursor' do - let(:projects_query) { 'projects(after: "invalid==")' } - - it 'treats the invalid cursor as no cursor and returns list of jira projects' do - expect(jira_projects.size).to eq(2) - end end end end diff --git a/spec/requests/api/graphql/project/jira_service_spec.rb b/spec/requests/api/graphql/project/jira_service_spec.rb index 4ac598b789f..905a669bf0d 100644 --- a/spec/requests/api/graphql/project/jira_service_spec.rb +++ b/spec/requests/api/graphql/project/jira_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'query Jira service' do +RSpec.describe 'query Jira service' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/project/labels_query_spec.rb b/spec/requests/api/graphql/project/labels_query_spec.rb index ecc43e0a3db..eeaaaaee575 100644 --- a/spec/requests/api/graphql/project/labels_query_spec.rb +++ b/spec/requests/api/graphql/project/labels_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'getting project label information' do +RSpec.describe 'getting project label information' do include GraphqlHelpers let_it_be(:project) { create(:project, :public) } diff --git a/spec/requests/api/graphql/project/merge_request/diff_notes_spec.rb b/spec/requests/api/graphql/project/merge_request/diff_notes_spec.rb index c616310a72c..dd16b052e0e 100644 --- a/spec/requests/api/graphql/project/merge_request/diff_notes_spec.rb +++ b/spec/requests/api/graphql/project/merge_request/diff_notes_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'getting notes for a merge request' do +RSpec.describe 'getting notes for a merge request' do include GraphqlHelpers let_it_be(:noteable) { create(:merge_request) } diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb index 643532bf2e2..c39358a2db1 100644 --- a/spec/requests/api/graphql/project/merge_request_spec.rb +++ b/spec/requests/api/graphql/project/merge_request_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'getting merge request information nested in a project' do +RSpec.describe 'getting merge request information nested in a project' do include GraphqlHelpers let(:project) { create(:project, :repository, :public) } @@ -43,6 +43,59 @@ describe 'getting merge request information nested in a project' do expect(merge_request_graphql_data['author']['username']).to eq(merge_request.author.username) end + it 'includes diff stats' do + be_natural = an_instance_of(Integer).and(be >= 0) + + post_graphql(query, current_user: current_user) + + sums = merge_request_graphql_data['diffStats'].reduce([0, 0, 0]) do |(a, d, c), node| + a_, d_ = node.values_at('additions', 'deletions') + [a + a_, d + d_, c + a_ + d_] + end + + expect(merge_request_graphql_data).to include( + 'diffStats' => all(a_hash_including('path' => String, 'additions' => be_natural, 'deletions' => be_natural)), + 'diffStatsSummary' => a_hash_including( + 'fileCount' => merge_request.diff_stats.count, + 'additions' => be_natural, + 'deletions' => be_natural, + 'changes' => be_natural + ) + ) + + # diff_stats is consistent with summary + expect(merge_request_graphql_data['diffStatsSummary'] + .values_at('additions', 'deletions', 'changes')).to eq(sums) + + # diff_stats_summary is internally consistent + expect(merge_request_graphql_data['diffStatsSummary'] + .values_at('additions', 'deletions').sum) + .to eq(merge_request_graphql_data.dig('diffStatsSummary', 'changes')) + .and be_positive + end + + context 'requesting a specific diff stat' do + let(:diff_stat) { merge_request.diff_stats.first } + + let(:query) do + graphql_query_for(:project, { full_path: project.full_path }, + query_graphql_field(:merge_request, { iid: merge_request.iid.to_s }, [ + query_graphql_field(:diff_stats, { path: diff_stat.path }, all_graphql_fields_for('DiffStats')) + ]) + ) + end + + it 'includes only the requested stats' do + post_graphql(query, current_user: current_user) + + expect(merge_request_graphql_data).to include( + 'diffStats' => contain_exactly( + a_hash_including('path' => diff_stat.path, 'additions' => diff_stat.additions, 'deletions' => diff_stat.deletions) + ) + ) + end + end + it 'includes correct mergedAt value when merged' do time = 1.week.ago merge_request.mark_as_merged diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb index 49fdfe29874..e2255fdb048 100644 --- a/spec/requests/api/graphql/project/merge_requests_spec.rb +++ b/spec/requests/api/graphql/project/merge_requests_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'getting merge request listings nested in a project' do +RSpec.describe 'getting merge request listings nested in a project' do include GraphqlHelpers let_it_be(:project) { create(:project, :repository, :public) } diff --git a/spec/requests/api/graphql/project/packages_spec.rb b/spec/requests/api/graphql/project/packages_spec.rb new file mode 100644 index 00000000000..88f97f9256b --- /dev/null +++ b/spec/requests/api/graphql/project/packages_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting a package list for a project' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:current_user) { create(:user) } + let_it_be(:package) { create(:package, project: project) } + let(:packages_data) { graphql_data['project']['packages']['edges'] } + + let(:fields) do + <<~QUERY + edges { + node { + #{all_graphql_fields_for('packages'.classify)} + } + } + QUERY + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('packages', {}, fields) + ) + end + + context 'without the need for a license' do + context 'when user has access to the project' do + before do + project.add_reporter(current_user) + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + + it 'returns packages successfully' do + expect(packages_data[0]['node']['name']).to eq package.name + end + end + + context 'when the user does not have access to the project/packages' do + before do + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + + it 'returns nil' do + expect(graphql_data['project']).to be_nil + end + end + + context 'when the user is not autenthicated' do + before do + post_graphql(query) + end + + it_behaves_like 'a working graphql query' + + it 'returns nil' do + expect(graphql_data['project']).to be_nil + end + end + end +end diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb index bed9a18577f..57b9de25c3d 100644 --- a/spec/requests/api/graphql/project/pipeline_spec.rb +++ b/spec/requests/api/graphql/project/pipeline_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'getting pipeline information nested in a project' do +RSpec.describe 'getting pipeline information nested in a project' do include GraphqlHelpers let(:project) { create(:project, :repository, :public) } diff --git a/spec/requests/api/graphql/project/project_statistics_spec.rb b/spec/requests/api/graphql/project/project_statistics_spec.rb index 05dd5d36c26..c226b10ab51 100644 --- a/spec/requests/api/graphql/project/project_statistics_spec.rb +++ b/spec/requests/api/graphql/project/project_statistics_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'rendering project statistics' do +RSpec.describe 'rendering project statistics' do include GraphqlHelpers let(:project) { create(:project) } diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb index f8624a97a2b..f9c19d9747d 100644 --- a/spec/requests/api/graphql/project/release_spec.rb +++ b/spec/requests/api/graphql/project/release_spec.rb @@ -1,206 +1,374 @@ # frozen_string_literal: true require 'spec_helper' -require 'pp' -describe 'Query.project(fullPath).release(tagName)' do +RSpec.describe 'Query.project(fullPath).release(tagName)' do include GraphqlHelpers include Presentable - let_it_be(:project) { create(:project, :repository) } - let_it_be(:milestone_1) { create(:milestone, project: project) } - let_it_be(:milestone_2) { create(:milestone, project: project) } - let_it_be(:release) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2]) } - let_it_be(:release_link_1) { create(:release_link, release: release) } - let_it_be(:release_link_2) { create(:release_link, release: release) } let_it_be(:developer) { create(:user) } + let_it_be(:guest) { create(:user) } + let_it_be(:reporter) { create(:user) } + let_it_be(:stranger) { create(:user) } - let(:current_user) { developer } + let(:params_for_issues_and_mrs) { { scope: 'all', state: 'opened', release_tag: release.tag } } + let(:post_query) { post_graphql(query, current_user: current_user) } + let(:path_prefix) { %w[project release] } + let(:data) { graphql_data.dig(*path) } def query(rq = release_fields) graphql_query_for(:project, { fullPath: project.full_path }, query_graphql_field(:release, { tagName: release.tag }, rq)) end - let(:post_query) { post_graphql(query, current_user: current_user) } - let(:path_prefix) { %w[project release] } - - let(:data) { graphql_data.dig(*path) } - before do - project.add_developer(developer) + stub_default_url_options(host: 'www.example.com') end - describe 'scalar fields' do - let(:path) { path_prefix } - let(:release_fields) do - query_graphql_field(%{ - tagName - tagPath - description - descriptionHtml - name - createdAt - releasedAt - }) + shared_examples 'full access to the release field' do + describe 'scalar fields' do + let(:path) { path_prefix } + + let(:release_fields) do + query_graphql_field(%{ + tagName + tagPath + description + descriptionHtml + name + createdAt + releasedAt + }) + end + + before do + post_query + end + + it 'finds all release data' do + expect(data).to eq({ + 'tagName' => release.tag, + 'tagPath' => project_tag_path(project, release.tag), + 'description' => release.description, + 'descriptionHtml' => release.description_html, + 'name' => release.name, + 'createdAt' => release.created_at.iso8601, + 'releasedAt' => release.released_at.iso8601 + }) + end end - before do - post_query + describe 'milestones' do + let(:path) { path_prefix + %w[milestones nodes] } + + let(:release_fields) do + query_graphql_field(:milestones, nil, 'nodes { id title }') + end + + it 'finds all milestones associated to a release' do + post_query + + expected = release.milestones.map do |milestone| + { 'id' => global_id_of(milestone), 'title' => milestone.title } + end + + expect(data).to match_array(expected) + end end - it 'finds all release data' do - expect(data).to eq({ - 'tagName' => release.tag, - 'tagPath' => project_tag_path(project, release.tag), - 'description' => release.description, - 'descriptionHtml' => release.description_html, - 'name' => release.name, - 'createdAt' => release.created_at.iso8601, - 'releasedAt' => release.released_at.iso8601 - }) + describe 'author' do + let(:path) { path_prefix + %w[author] } + + let(:release_fields) do + query_graphql_field(:author, nil, 'id username') + end + + it 'finds the author of the release' do + post_query + + expect(data).to eq( + 'id' => global_id_of(release.author), + 'username' => release.author.username + ) + end end - end - describe 'milestones' do - let(:path) { path_prefix + %w[milestones nodes] } - let(:release_fields) do - query_graphql_field(:milestones, nil, 'nodes { id title }') + describe 'commit' do + let(:path) { path_prefix + %w[commit] } + + let(:release_fields) do + query_graphql_field(:commit, nil, 'sha') + end + + it 'finds the commit associated with the release' do + post_query + + expect(data).to eq('sha' => release.commit.sha) + end end - it 'finds all milestones associated to a release' do - post_query + describe 'assets' do + describe 'count' do + let(:path) { path_prefix + %w[assets] } + + let(:release_fields) do + query_graphql_field(:assets, nil, 'count') + end + + it 'returns the number of assets associated to the release' do + post_query + + expect(data).to eq('count' => release.sources.size + release.links.size) + end + end + + describe 'links' do + let(:path) { path_prefix + %w[assets links nodes] } - expected = release.milestones.map do |milestone| - { 'id' => global_id_of(milestone), 'title' => milestone.title } + let(:release_fields) do + query_graphql_field(:assets, nil, + query_graphql_field(:links, nil, 'nodes { id name url external }')) + end + + it 'finds all release links' do + post_query + + expected = release.links.map do |link| + { + 'id' => global_id_of(link), + 'name' => link.name, + 'url' => link.url, + 'external' => link.external? + } + end + + expect(data).to match_array(expected) + end end - expect(data).to match_array(expected) + describe 'sources' do + let(:path) { path_prefix + %w[assets sources nodes] } + + let(:release_fields) do + query_graphql_field(:assets, nil, + query_graphql_field(:sources, nil, 'nodes { format url }')) + end + + it 'finds all release sources' do + post_query + + expected = release.sources.map do |source| + { + 'format' => source.format, + 'url' => source.url + } + end + + expect(data).to match_array(expected) + end + end + end + + describe 'links' do + let(:path) { path_prefix + %w[links] } + + let(:release_fields) do + query_graphql_field(:links, nil, %{ + selfUrl + mergeRequestsUrl + issuesUrl + }) + end + + it 'finds all release links' do + post_query + + expect(data).to eq( + 'selfUrl' => project_release_url(project, release), + 'mergeRequestsUrl' => project_merge_requests_url(project, params_for_issues_and_mrs), + 'issuesUrl' => project_issues_url(project, params_for_issues_and_mrs) + ) + end + end + + describe 'evidences' do + let(:path) { path_prefix + %w[evidences] } + + let(:release_fields) do + query_graphql_field(:evidences, nil, 'nodes { id sha filepath collectedAt }') + end + + it 'finds all evidence fields' do + post_query + + evidence = release.evidences.first.present + + expect(data["nodes"].first).to eq( + 'id' => global_id_of(evidence), + 'sha' => evidence.sha, + 'filepath' => evidence.filepath, + 'collectedAt' => evidence.collected_at.utc.iso8601 + ) + end + end + end + + shared_examples 'no access to the release field' do + describe 'repository-related fields' do + let(:path) { path_prefix } + + let(:release_fields) do + query_graphql_field('description') + end + + before do + post_query + end + + it 'returns nil' do + expect(data).to eq(nil) + end end end - describe 'author' do - let(:path) { path_prefix + %w[author] } + shared_examples 'access to editUrl' do + let(:path) { path_prefix + %w[links] } + let(:release_fields) do - query_graphql_field(:author, nil, 'id username') + query_graphql_field(:links, nil, 'editUrl') end - it 'finds the author of the release' do + before do post_query + end - expect(data).to eq({ - 'id' => global_id_of(release.author), - 'username' => release.author.username - }) + it 'returns editUrl' do + expect(data).to eq('editUrl' => edit_project_release_url(project, release)) end end - describe 'commit' do - let(:path) { path_prefix + %w[commit] } + shared_examples 'no access to editUrl' do + let(:path) { path_prefix + %w[links] } + let(:release_fields) do - query_graphql_field(:commit, nil, 'sha') + query_graphql_field(:links, nil, 'editUrl') end - it 'finds the commit associated with the release' do + before do post_query + end - expect(data).to eq({ 'sha' => release.commit.sha }) + it 'does not return editUrl' do + expect(data).to eq('editUrl' => nil) end end - describe 'assets' do - describe 'assetsCount' do - let(:path) { path_prefix + %w[assets] } - let(:release_fields) do - query_graphql_field(:assets, nil, 'assetsCount') + describe "ensures that the correct data is returned based on the project's visibility and the user's access level" do + context 'when the project is private' do + let_it_be(:project) { create(:project, :repository, :private) } + let_it_be(:milestone_1) { create(:milestone, project: project) } + let_it_be(:milestone_2) { create(:milestone, project: project) } + let_it_be(:release) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2]) } + let_it_be(:release_link_1) { create(:release_link, release: release) } + let_it_be(:release_link_2) { create(:release_link, release: release) } + + before_all do + project.add_developer(developer) + project.add_guest(guest) + project.add_reporter(reporter) end - it 'returns the number of assets associated to the release' do - post_query + context 'when the user is not logged in' do + let(:current_user) { stranger } - expect(data).to eq({ 'assetsCount' => release.sources.size + release.links.size }) + it_behaves_like 'no access to the release field' end - end - describe 'links' do - let(:path) { path_prefix + %w[assets links nodes] } - let(:release_fields) do - query_graphql_field(:assets, nil, - query_graphql_field(:links, nil, 'nodes { id name url external }')) + context 'when the user has Guest permissions' do + let(:current_user) { guest } + + it_behaves_like 'no access to the release field' end - it 'finds all release links' do - post_query + context 'when the user has Reporter permissions' do + let(:current_user) { reporter } - expected = release.links.map do |link| - { - 'id' => global_id_of(link), - 'name' => link.name, - 'url' => link.url, - 'external' => link.external? - } - end + it_behaves_like 'full access to the release field' + it_behaves_like 'no access to editUrl' + end - expect(data).to match_array(expected) + context 'when the user has Developer permissions' do + let(:current_user) { developer } + + it_behaves_like 'full access to the release field' + it_behaves_like 'access to editUrl' end end - describe 'sources' do - let(:path) { path_prefix + %w[assets sources nodes] } - let(:release_fields) do - query_graphql_field(:assets, nil, - query_graphql_field(:sources, nil, 'nodes { format url }')) + context 'when the project is public' do + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:milestone_1) { create(:milestone, project: project) } + let_it_be(:milestone_2) { create(:milestone, project: project) } + let_it_be(:release) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2]) } + let_it_be(:release_link_1) { create(:release_link, release: release) } + let_it_be(:release_link_2) { create(:release_link, release: release) } + + before_all do + project.add_developer(developer) + project.add_guest(guest) + project.add_reporter(reporter) end - it 'finds all release sources' do - post_query + context 'when the user is not logged in' do + let(:current_user) { stranger } - expected = release.sources.map do |source| - { - 'format' => source.format, - 'url' => source.url - } - end + it_behaves_like 'full access to the release field' + it_behaves_like 'no access to editUrl' + end - expect(data).to match_array(expected) + context 'when the user has Guest permissions' do + let(:current_user) { guest } + + it_behaves_like 'full access to the release field' + it_behaves_like 'no access to editUrl' end - end - describe 'evidences' do - let(:path) { path_prefix + %w[evidences] } - let(:release_fields) do - query_graphql_field(:evidences, nil, 'nodes { id sha filepath collectedAt }') + context 'when the user has Reporter permissions' do + let(:current_user) { reporter } + + it_behaves_like 'full access to the release field' + it_behaves_like 'no access to editUrl' end - context 'for a developer' do - it 'finds all evidence fields' do - post_query + context 'when the user has Reporter permissions' do + let(:current_user) { reporter } - evidence = release.evidences.first.present - expected = { - 'id' => global_id_of(evidence), - 'sha' => evidence.sha, - 'filepath' => evidence.filepath, - 'collectedAt' => evidence.collected_at.utc.iso8601 - } + it_behaves_like 'full access to the release field' + end - expect(data["nodes"].first).to eq(expected) - end + context 'when the user has Developer permissions' do + let(:current_user) { developer } + + it_behaves_like 'full access to the release field' + it_behaves_like 'access to editUrl' end + end + end - context 'for a guest' do - let(:current_user) { create :user } + describe 'ensures that the release data can be contolled by a feature flag' do + context 'when the graphql_release_data feature flag is disabled' do + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:release) { create(:release, project: project) } - before do - project.add_guest(current_user) - end + let(:current_user) { developer } - it 'denies access' do - post_query + before do + stub_feature_flags(graphql_release_data: false) - expect(data['node']).to be_nil - end + project.add_developer(developer) end + + it_behaves_like 'no access to the release field' end end end diff --git a/spec/requests/api/graphql/project/releases_spec.rb b/spec/requests/api/graphql/project/releases_spec.rb new file mode 100644 index 00000000000..7e418bbaa5b --- /dev/null +++ b/spec/requests/api/graphql/project/releases_spec.rb @@ -0,0 +1,284 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.project(fullPath).releases()' do + include GraphqlHelpers + + let_it_be(:stranger) { create(:user) } + let_it_be(:guest) { create(:user) } + let_it_be(:reporter) { create(:user) } + let_it_be(:developer) { create(:user) } + + let(:query) do + graphql_query_for(:project, { fullPath: project.full_path }, + %{ + releases { + nodes { + tagName + tagPath + name + commit { + sha + } + assets { + count + sources { + nodes { + url + } + } + } + evidences { + nodes { + sha + } + } + links { + selfUrl + mergeRequestsUrl + issuesUrl + } + } + } + }) + end + + let(:params_for_issues_and_mrs) { { scope: 'all', state: 'opened', release_tag: release.tag } } + let(:post_query) { post_graphql(query, current_user: current_user) } + + let(:data) { graphql_data.dig('project', 'releases', 'nodes', 0) } + + before do + stub_default_url_options(host: 'www.example.com') + end + + shared_examples 'full access to all repository-related fields' do + describe 'repository-related fields' do + before do + post_query + end + + it 'returns data for fields that are protected in private projects' do + expected_sources = release.sources.map do |s| + { 'url' => s.url } + end + + expected_evidences = release.evidences.map do |e| + { 'sha' => e.sha } + end + + expect(data).to eq( + 'tagName' => release.tag, + 'tagPath' => project_tag_path(project, release.tag), + 'name' => release.name, + 'commit' => { + 'sha' => release.commit.sha + }, + 'assets' => { + 'count' => release.assets_count, + 'sources' => { + 'nodes' => expected_sources + } + }, + 'evidences' => { + 'nodes' => expected_evidences + }, + 'links' => { + 'selfUrl' => project_release_url(project, release), + 'mergeRequestsUrl' => project_merge_requests_url(project, params_for_issues_and_mrs), + 'issuesUrl' => project_issues_url(project, params_for_issues_and_mrs) + } + ) + end + end + end + + shared_examples 'no access to any repository-related fields' do + describe 'repository-related fields' do + before do + post_query + end + + it 'does not return data for fields that expose repository information' do + expect(data).to eq( + 'tagName' => nil, + 'tagPath' => nil, + 'name' => "Release-#{release.id}", + 'commit' => nil, + 'assets' => { + 'count' => release.assets_count(except: [:sources]), + 'sources' => { + 'nodes' => [] + } + }, + 'evidences' => { + 'nodes' => [] + }, + 'links' => nil + ) + end + end + end + + # editUrl is tested separately becuase its permissions + # are slightly different than other release fields + shared_examples 'access to editUrl' do + let(:query) do + graphql_query_for(:project, { fullPath: project.full_path }, + %{ + releases { + nodes { + links { + editUrl + } + } + } + }) + end + + before do + post_query + end + + it 'returns editUrl' do + expect(data).to eq( + 'links' => { + 'editUrl' => edit_project_release_url(project, release) + } + ) + end + end + + shared_examples 'no access to editUrl' do + let(:query) do + graphql_query_for(:project, { fullPath: project.full_path }, + %{ + releases { + nodes { + links { + editUrl + } + } + } + }) + end + + before do + post_query + end + + it 'does not return editUrl' do + expect(data).to eq( + 'links' => { + 'editUrl' => nil + } + ) + end + end + + shared_examples 'no access to any release data' do + before do + post_query + end + + it 'returns nil' do + expect(data).to eq(nil) + end + end + + describe "ensures that the correct data is returned based on the project's visibility and the user's access level" do + context 'when the project is private' do + let_it_be(:project) { create(:project, :repository, :private) } + let_it_be(:release) { create(:release, :with_evidence, project: project) } + + before_all do + project.add_guest(guest) + project.add_reporter(reporter) + project.add_developer(developer) + end + + context 'when the user is not logged in' do + let(:current_user) { stranger } + + it_behaves_like 'no access to any release data' + end + + context 'when the user has Guest permissions' do + let(:current_user) { guest } + + it_behaves_like 'no access to any repository-related fields' + end + + context 'when the user has Reporter permissions' do + let(:current_user) { reporter } + + it_behaves_like 'full access to all repository-related fields' + it_behaves_like 'no access to editUrl' + end + + context 'when the user has Developer permissions' do + let(:current_user) { developer } + + it_behaves_like 'full access to all repository-related fields' + it_behaves_like 'access to editUrl' + end + end + + context 'when the project is public' do + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:release) { create(:release, :with_evidence, project: project) } + + before_all do + project.add_guest(guest) + project.add_reporter(reporter) + project.add_developer(developer) + end + + context 'when the user is not logged in' do + let(:current_user) { stranger } + + it_behaves_like 'full access to all repository-related fields' + it_behaves_like 'no access to editUrl' + end + + context 'when the user has Guest permissions' do + let(:current_user) { guest } + + it_behaves_like 'full access to all repository-related fields' + it_behaves_like 'no access to editUrl' + end + + context 'when the user has Reporter permissions' do + let(:current_user) { reporter } + + it_behaves_like 'full access to all repository-related fields' + it_behaves_like 'no access to editUrl' + end + + context 'when the user has Developer permissions' do + let(:current_user) { developer } + + it_behaves_like 'full access to all repository-related fields' + it_behaves_like 'access to editUrl' + end + end + end + + describe 'ensures that the release data can be contolled by a feature flag' do + context 'when the graphql_release_data feature flag is disabled' do + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:release) { create(:release, project: project) } + + let(:current_user) { developer } + + before do + stub_feature_flags(graphql_release_data: false) + + project.add_developer(developer) + end + + it_behaves_like 'no access to any release data' + end + end +end diff --git a/spec/requests/api/graphql/project/repository_spec.rb b/spec/requests/api/graphql/project/repository_spec.rb index 261433a3d6a..bd719a69647 100644 --- a/spec/requests/api/graphql/project/repository_spec.rb +++ b/spec/requests/api/graphql/project/repository_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -describe 'getting a repository in a project' do +RSpec.describe 'getting a repository in a project' do include GraphqlHelpers let(:project) { create(:project, :repository) } diff --git a/spec/requests/api/graphql/project/tree/tree_spec.rb b/spec/requests/api/graphql/project/tree/tree_spec.rb index 94128cc21ee..bce63d57c38 100644 --- a/spec/requests/api/graphql/project/tree/tree_spec.rb +++ b/spec/requests/api/graphql/project/tree/tree_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -describe 'getting a tree in a project' do +RSpec.describe 'getting a tree in a project' do include GraphqlHelpers let(:project) { create(:project, :repository) } diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb index 9a88b47eea6..b115030afbc 100644 --- a/spec/requests/api/graphql/project_query_spec.rb +++ b/spec/requests/api/graphql/project_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'getting project information' do +RSpec.describe 'getting project information' do include GraphqlHelpers let(:project) { create(:project, :repository) } diff --git a/spec/requests/api/graphql/query_spec.rb b/spec/requests/api/graphql/query_spec.rb index 26b4c6eafd7..6bd0703c121 100644 --- a/spec/requests/api/graphql/query_spec.rb +++ b/spec/requests/api/graphql/query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Query' do +RSpec.describe 'Query' do include GraphqlHelpers let_it_be(:project) { create(:project) } diff --git a/spec/requests/api/graphql/read_only_spec.rb b/spec/requests/api/graphql/read_only_spec.rb index 1d28a71258d..ce8a3f6ef5c 100644 --- a/spec/requests/api/graphql/read_only_spec.rb +++ b/spec/requests/api/graphql/read_only_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Requests on a read-only node' do +RSpec.describe 'Requests on a read-only node' do include GraphqlHelpers before do diff --git a/spec/requests/api/graphql/tasks/task_completion_status_spec.rb b/spec/requests/api/graphql/tasks/task_completion_status_spec.rb index c47406ea534..5f4d2aec718 100644 --- a/spec/requests/api/graphql/tasks/task_completion_status_spec.rb +++ b/spec/requests/api/graphql/tasks/task_completion_status_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'getting task completion status information' do +RSpec.describe 'getting task completion status information' do include GraphqlHelpers description_0_done = '- [ ] task 1\n- [ ] task 2' diff --git a/spec/requests/api/graphql/user/group_member_query_spec.rb b/spec/requests/api/graphql/user/group_member_query_spec.rb index 022ee79297c..3a16d962214 100644 --- a/spec/requests/api/graphql/user/group_member_query_spec.rb +++ b/spec/requests/api/graphql/user/group_member_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'GroupMember' do +RSpec.describe 'GroupMember' do include GraphqlHelpers let_it_be(:member) { create(:group_member, :developer) } diff --git a/spec/requests/api/graphql/user/project_member_query_spec.rb b/spec/requests/api/graphql/user/project_member_query_spec.rb index 397d2872189..0790e148caf 100644 --- a/spec/requests/api/graphql/user/project_member_query_spec.rb +++ b/spec/requests/api/graphql/user/project_member_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'ProjectMember' do +RSpec.describe 'ProjectMember' do include GraphqlHelpers let_it_be(:member) { create(:project_member, :developer) } diff --git a/spec/requests/api/graphql/user_query_spec.rb b/spec/requests/api/graphql/user_query_spec.rb index 5ac94bc7323..7ba1788a9ef 100644 --- a/spec/requests/api/graphql/user_query_spec.rb +++ b/spec/requests/api/graphql/user_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'getting user information' do +RSpec.describe 'getting user information' do include GraphqlHelpers let(:query) do diff --git a/spec/requests/api/graphql/user_spec.rb b/spec/requests/api/graphql/user_spec.rb index 097c75b3541..d2d6b1fca66 100644 --- a/spec/requests/api/graphql/user_spec.rb +++ b/spec/requests/api/graphql/user_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'User' do +RSpec.describe 'User' do include GraphqlHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/requests/api/graphql/users_spec.rb b/spec/requests/api/graphql/users_spec.rb index 1e6d73cbd7d..91ac206676b 100644 --- a/spec/requests/api/graphql/users_spec.rb +++ b/spec/requests/api/graphql/users_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Users' do +RSpec.describe 'Users' do include GraphqlHelpers let_it_be(:current_user) { create(:user, created_at: 1.day.ago) } diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb index 84be5ab0951..ff1a5aa1540 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' -describe 'GraphQL' do +RSpec.describe 'GraphQL' do include GraphqlHelpers let(:query) { graphql_query_for('echo', 'text' => 'Hello world' ) } diff --git a/spec/requests/api/group_boards_spec.rb b/spec/requests/api/group_boards_spec.rb index a9083f82f25..6ce8b766807 100644 --- a/spec/requests/api/group_boards_spec.rb +++ b/spec/requests/api/group_boards_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::GroupBoards do +RSpec.describe API::GroupBoards do let_it_be(:user) { create(:user) } let_it_be(:non_member) { create(:user) } let_it_be(:guest) { create(:user) } diff --git a/spec/requests/api/group_clusters_spec.rb b/spec/requests/api/group_clusters_spec.rb index fade54f6b11..068af1485e2 100644 --- a/spec/requests/api/group_clusters_spec.rb +++ b/spec/requests/api/group_clusters_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::GroupClusters do +RSpec.describe API::GroupClusters do include KubernetesHelpers let(:current_user) { create(:user) } @@ -266,29 +266,51 @@ describe API::GroupClusters do end end - context 'when user tries to add multiple clusters' do + context 'non-authorized user' do before do - create(:cluster, :provided_by_gcp, :group, - groups: [group]) - - post api("/groups/#{group.id}/clusters/user", current_user), params: cluster_params + post api("/groups/#{group.id}/clusters/user", developer_user), params: cluster_params end - it 'responds with 400' do - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']['base'].first).to eq(_('Instance does not support multiple Kubernetes clusters')) + it 'responds with 403' do + expect(response).to have_gitlab_http_status(:forbidden) + + expect(json_response['message']).to eq('403 Forbidden') end end + end - context 'non-authorized user' do + describe 'PUT /groups/:id/clusters/:cluster_id' do + let(:api_url) { 'https://kubernetes.example.com' } + + let(:platform_kubernetes_attributes) do + { + api_url: api_url, + token: 'sample-token' + } + end + + let(:cluster_params) do + { + name: 'test-cluster', + environment_scope: 'test/*', + platform_kubernetes_attributes: platform_kubernetes_attributes + } + end + + context 'when another cluster exists' do before do - post api("/groups/#{group.id}/clusters/user", developer_user), params: cluster_params + create(:cluster, :provided_by_gcp, :group, + groups: [group]) + + post api("/groups/#{group.id}/clusters/user", current_user), params: cluster_params end - it 'responds with 403' do - expect(response).to have_gitlab_http_status(:forbidden) + it 'responds with 201' do + expect(response).to have_gitlab_http_status(:created) + end - expect(json_response['message']).to eq('403 Forbidden') + it 'allows multiple clusters to be associated to group' do + expect(group.reload.clusters.count).to eq(2) end end end diff --git a/spec/requests/api/group_container_repositories_spec.rb b/spec/requests/api/group_container_repositories_spec.rb index 9f439bb2167..3128becae6d 100644 --- a/spec/requests/api/group_container_repositories_spec.rb +++ b/spec/requests/api/group_container_repositories_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::GroupContainerRepositories do +RSpec.describe API::GroupContainerRepositories do let_it_be(:group) { create(:group, :private) } let_it_be(:project) { create(:project, :private, group: group) } let_it_be(:reporter) { create(:user) } diff --git a/spec/requests/api/group_export_spec.rb b/spec/requests/api/group_export_spec.rb index 9dd7797c768..50a1e9d0c3d 100644 --- a/spec/requests/api/group_export_spec.rb +++ b/spec/requests/api/group_export_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::GroupExport do +RSpec.describe API::GroupExport do let_it_be(:group) { create(:group) } let_it_be(:user) { create(:user) } @@ -33,6 +33,10 @@ describe API::GroupExport do context 'group_import_export feature flag enabled' do before do stub_feature_flags(group_import_export: true) + + allow(Gitlab::ApplicationRateLimiter) + .to receive(:increment) + .and_return(0) end context 'when export file exists' do @@ -87,7 +91,7 @@ describe API::GroupExport do before do allow(Gitlab::ApplicationRateLimiter) .to receive(:increment) - .and_return(Gitlab::ApplicationRateLimiter.rate_limits[:group_download_export][:threshold] + 1) + .and_return(Gitlab::ApplicationRateLimiter.rate_limits[:group_download_export][:threshold].call + 1) end it 'throttles the endpoint' do @@ -162,7 +166,7 @@ describe API::GroupExport do allow(Gitlab::ApplicationRateLimiter) .to receive(:increment) - .and_return(Gitlab::ApplicationRateLimiter.rate_limits[:group_export][:threshold] + 1) + .and_return(Gitlab::ApplicationRateLimiter.rate_limits[:group_export][:threshold].call + 1) end it 'throttles the endpoint' do diff --git a/spec/requests/api/group_import_spec.rb b/spec/requests/api/group_import_spec.rb index b60a1b3f119..ad67f737725 100644 --- a/spec/requests/api/group_import_spec.rb +++ b/spec/requests/api/group_import_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::GroupImport do +RSpec.describe API::GroupImport do include WorkhorseHelpers let_it_be(:user) { create(:user) } @@ -122,6 +122,7 @@ describe API::GroupImport do before do allow_next_instance_of(Group) do |group| allow(group).to receive(:persisted?).and_return(false) + allow(group).to receive(:save).and_return(false) end end diff --git a/spec/requests/api/group_labels_spec.rb b/spec/requests/api/group_labels_spec.rb index 715c1255cb3..f965a845bbe 100644 --- a/spec/requests/api/group_labels_spec.rb +++ b/spec/requests/api/group_labels_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::GroupLabels do +RSpec.describe API::GroupLabels do let(:user) { create(:user) } let(:group) { create(:group) } let(:subgroup) { create(:group, parent: group) } diff --git a/spec/requests/api/group_milestones_spec.rb b/spec/requests/api/group_milestones_spec.rb index 3e9b6246434..2b361f2b503 100644 --- a/spec/requests/api/group_milestones_spec.rb +++ b/spec/requests/api/group_milestones_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::GroupMilestones do +RSpec.describe API::GroupMilestones do let(:user) { create(:user) } let(:group) { create(:group, :private) } let(:project) { create(:project, namespace: group) } diff --git a/spec/requests/api/group_packages_spec.rb b/spec/requests/api/group_packages_spec.rb new file mode 100644 index 00000000000..7c7e8da3fb1 --- /dev/null +++ b/spec/requests/api/group_packages_spec.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::GroupPackages do + let_it_be(:group) { create(:group, :public) } + let_it_be(:project) { create(:project, :public, namespace: group, name: 'project A') } + let_it_be(:user) { create(:user) } + + subject { get api(url) } + + describe 'GET /groups/:id/packages' do + let(:url) { "/groups/#{group.id}/packages" } + let(:package_schema) { 'public_api/v4/packages/group_packages' } + + context 'without the need for a license' do + context 'with sorting' do + let_it_be(:package1) { create(:npm_package, project: project, version: '3.1.0', name: "@#{project.root_namespace.path}/foo1") } + let_it_be(:package2) { create(:nuget_package, project: project, version: '2.0.4') } + let(:package3) { create(:maven_package, project: project, version: '1.1.1', name: 'zzz') } + + before do + travel_to(1.day.ago) do + package3 + end + end + + context 'without sorting params' do + let(:packages) { [package3, package1, package2] } + + it 'sorts by created_at asc' do + subject + + expect(json_response.map { |package| package['id'] }).to eq(packages.map(&:id)) + end + end + + it_behaves_like 'package sorting', 'name' do + let(:packages) { [package1, package2, package3] } + end + + it_behaves_like 'package sorting', 'created_at' do + let(:packages) { [package3, package1, package2] } + end + + it_behaves_like 'package sorting', 'version' do + let(:packages) { [package3, package2, package1] } + end + + it_behaves_like 'package sorting', 'type' do + let(:packages) { [package3, package1, package2] } + end + + it_behaves_like 'package sorting', 'project_path' do + let(:another_project) { create(:project, :public, namespace: group, name: 'project B') } + let!(:package4) { create(:npm_package, project: another_project, version: '3.1.0', name: "@#{project.root_namespace.path}/bar") } + + let(:packages) { [package1, package2, package3, package4] } + end + end + + context 'with private group' do + let!(:package1) { create(:package, project: project) } + let!(:package2) { create(:package, project: project) } + + let(:group) { create(:group, :private) } + let(:subgroup) { create(:group, :private, parent: group) } + let(:project) { create(:project, :private, namespace: group) } + let(:subproject) { create(:project, :private, namespace: subgroup) } + + context 'with unauthenticated user' do + it_behaves_like 'rejects packages access', :group, :no_type, :not_found + end + + context 'with authenticated user' do + subject { get api(url, user) } + + it_behaves_like 'returns packages', :group, :owner + it_behaves_like 'returns packages', :group, :maintainer + it_behaves_like 'returns packages', :group, :developer + it_behaves_like 'rejects packages access', :group, :reporter, :forbidden + it_behaves_like 'rejects packages access', :group, :guest, :forbidden + + context 'with subgroup' do + let(:subgroup) { create(:group, :private, parent: group) } + let(:subproject) { create(:project, :private, namespace: subgroup) } + let!(:package3) { create(:npm_package, project: subproject) } + + it_behaves_like 'returns packages with subgroups', :group, :owner + it_behaves_like 'returns packages with subgroups', :group, :maintainer + it_behaves_like 'returns packages with subgroups', :group, :developer + it_behaves_like 'rejects packages access', :group, :reporter, :forbidden + it_behaves_like 'rejects packages access', :group, :guest, :forbidden + + context 'excluding subgroup' do + let(:url) { "/groups/#{group.id}/packages?exclude_subgroups=true" } + + it_behaves_like 'returns packages', :group, :owner + it_behaves_like 'returns packages', :group, :maintainer + it_behaves_like 'returns packages', :group, :developer + it_behaves_like 'rejects packages access', :group, :reporter, :forbidden + it_behaves_like 'rejects packages access', :group, :guest, :forbidden + end + end + end + end + + context 'with public group' do + let_it_be(:package1) { create(:package, project: project) } + let_it_be(:package2) { create(:package, project: project) } + + context 'with unauthenticated user' do + it_behaves_like 'returns packages', :group, :no_type + end + + context 'with authenticated user' do + subject { get api(url, user) } + + it_behaves_like 'returns packages', :group, :owner + it_behaves_like 'returns packages', :group, :maintainer + it_behaves_like 'returns packages', :group, :developer + it_behaves_like 'returns packages', :group, :reporter + it_behaves_like 'returns packages', :group, :guest + end + end + + context 'with pagination params' do + let_it_be(:package1) { create(:package, project: project) } + let_it_be(:package2) { create(:package, project: project) } + let_it_be(:package3) { create(:npm_package, project: project) } + let_it_be(:package4) { create(:npm_package, project: project) } + + it_behaves_like 'returns paginated packages' + end + + it_behaves_like 'filters on each package_type', is_project: false + + context 'does not accept non supported package_type value' do + include_context 'package filter context' + + let(:url) { group_filter_url(:type, 'foo') } + + it_behaves_like 'returning response status', :bad_request + end + end + end +end diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb index a5b48985df5..c6d6ae1615b 100644 --- a/spec/requests/api/group_variables_spec.rb +++ b/spec/requests/api/group_variables_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::GroupVariables do +RSpec.describe API::GroupVariables do let(:group) { create(:group) } let(:user) { create(:user) } diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 9a449499576..fac9f4dfe00 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Groups do +RSpec.describe API::Groups do include GroupAPIHelpers include UploadHelpers @@ -15,6 +15,7 @@ describe API::Groups do let_it_be(:project1) { create(:project, namespace: group1) } let_it_be(:project2) { create(:project, namespace: group2) } let_it_be(:project3) { create(:project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) } + let_it_be(:archived_project) { create(:project, namespace: group1, archived: true) } before do group1.add_owner(user1) @@ -184,11 +185,12 @@ describe API::Groups do it "includes statistics if requested" do attributes = { - storage_size: 1158, + storage_size: 2392, repository_size: 123, wiki_size: 456, lfs_objects_size: 234, - build_artifacts_size: 345 + build_artifacts_size: 345, + snippets_size: 1234 }.stringify_keys exposed_attributes = attributes.dup exposed_attributes['job_artifacts_size'] = exposed_attributes.delete('build_artifacts_size') @@ -470,7 +472,7 @@ describe API::Groups do expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) expect(json_response['shared_with_groups'][0]).to have_key('expires_at') expect(json_response['projects']).to be_an Array - expect(json_response['projects'].length).to eq(2) + expect(json_response['projects'].length).to eq(3) expect(json_response['shared_projects']).to be_an Array expect(json_response['shared_projects'].length).to eq(1) expect(json_response['shared_projects'][0]['id']).to eq(project.id) @@ -695,7 +697,7 @@ describe API::Groups do expect(json_response['parent_id']).to eq(nil) expect(json_response['created_at']).to be_present expect(json_response['projects']).to be_an Array - expect(json_response['projects'].length).to eq(2) + expect(json_response['projects'].length).to eq(3) expect(json_response['shared_projects']).to be_an Array expect(json_response['shared_projects'].length).to eq(0) expect(json_response['default_branch_protection']).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) @@ -821,20 +823,51 @@ describe API::Groups do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers - expect(json_response.length).to eq(2) + expect(json_response.length).to eq(3) project_names = json_response.map { |proj| proj['name'] } - expect(project_names).to match_array([project1.name, project3.name]) + expect(project_names).to match_array([project1.name, project3.name, archived_project.name]) expect(json_response.first['visibility']).to be_present end + context 'and using archived' do + it "returns the group's archived projects" do + get api("/groups/#{group1.id}/projects?archived=true", user1) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(Project.public_or_visible_to_user(user1).where(archived: true).size) + expect(json_response.map { |project| project['id'] }).to include(archived_project.id) + end + + it "returns the group's non-archived projects" do + get api("/groups/#{group1.id}/projects?archived=false", user1) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(Project.public_or_visible_to_user(user1).where(archived: false).size) + expect(json_response.map { |project| project['id'] }).not_to include(archived_project.id) + end + + it "returns all of the group's projects" do + get api("/groups/#{group1.id}/projects", user1) + + 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 { |project| project['id'] }).to contain_exactly(*Project.public_or_visible_to_user(user1).pluck(:id)) + end + end + it "returns the group's projects with simple representation" do get api("/groups/#{group1.id}/projects", user1), params: { simple: true } expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers - expect(json_response.length).to eq(2) + expect(json_response.length).to eq(3) project_names = json_response.map { |proj| proj['name'] } - expect(project_names).to match_array([project1.name, project3.name]) + expect(project_names).to match_array([project1.name, project3.name, archived_project.name]) expect(json_response.first['visibility']).not_to be_present end @@ -860,7 +893,7 @@ describe API::Groups 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.length).to eq(2) + expect(json_response.length).to eq(3) end it "returns projects including those in subgroups" do @@ -873,7 +906,7 @@ describe API::Groups 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.length).to eq(4) + expect(json_response.length).to eq(5) end it "does not return a non existing group" do @@ -958,7 +991,7 @@ describe API::Groups do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers project_names = json_response.map { |proj| proj['name'] } - expect(project_names).to match_array([project1.name, project3.name]) + expect(project_names).to match_array([project1.name, project3.name, archived_project.name]) end it 'does not return a non existing group' do diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index d65c89f48ea..12cd5ace84e 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' -describe API::Helpers do +RSpec.describe API::Helpers do include API::APIGuard::HelperMethods include described_class include TermsHelper diff --git a/spec/requests/api/import_bitbucket_server_spec.rb b/spec/requests/api/import_bitbucket_server_spec.rb new file mode 100644 index 00000000000..5828dab3080 --- /dev/null +++ b/spec/requests/api/import_bitbucket_server_spec.rb @@ -0,0 +1,218 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::ImportBitbucketServer do + let(:base_uri) { "https://test:7990" } + let(:user) { create(:user) } + let(:token) { "asdasd12345" } + let(:secret) { "sekrettt" } + let(:project_key) { 'TES' } + let(:repo_slug) { 'vim' } + let(:repo) { { name: 'vim' } } + + describe "POST /import/bitbucket_server" do + context 'with no optional parameters' do + let_it_be(:project) { create(:project) } + let(:client) { double(BitbucketServer::Client) } + + before do + Grape::Endpoint.before_each do |endpoint| + allow(endpoint).to receive(:client).and_return(client.as_null_object) + allow(client).to receive(:repo).with(project_key, repo_slug).and_return(double(name: repo_slug)) + end + end + + after do + Grape::Endpoint.before_each nil + end + + it 'returns 201 response when the project is imported successfully' do + allow(Gitlab::BitbucketServerImport::ProjectCreator) + .to receive(:new).with(project_key, repo_slug, anything, repo_slug, user.namespace, user, anything) + .and_return(double(execute: project)) + + post api("/import/bitbucket_server", user), params: { + bitbucket_server_url: base_uri, + bitbucket_server_username: user, + personal_access_token: token, + bitbucket_server_project: project_key, + bitbucket_server_repo: repo_slug + } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to be_a Hash + expect(json_response['name']).to eq(project.name) + end + end + + context 'with a new project name' do + let_it_be(:project) { create(:project, name: 'new-name') } + let(:client) { instance_double(BitbucketServer::Client) } + + before do + Grape::Endpoint.before_each do |endpoint| + allow(endpoint).to receive(:client).and_return(client) + allow(client).to receive(:repo).with(project_key, repo_slug).and_return(double(name: repo_slug)) + end + end + + after do + Grape::Endpoint.before_each nil + end + + it 'returns 201 response when the project is imported successfully with a new project name' do + allow(Gitlab::BitbucketServerImport::ProjectCreator) + .to receive(:new).with(project_key, repo_slug, anything, project.name, user.namespace, user, anything) + .and_return(double(execute: project)) + + post api("/import/bitbucket_server", user), params: { + bitbucket_server_url: base_uri, + bitbucket_server_username: user, + personal_access_token: token, + bitbucket_server_project: project_key, + bitbucket_server_repo: repo_slug, + new_name: 'new-name' + } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to be_a Hash + expect(json_response['name']).to eq('new-name') + end + end + + context 'with an invalid URL' do + let_it_be(:project) { create(:project, name: 'new-name') } + let(:client) { instance_double(BitbucketServer::Client) } + + before do + Grape::Endpoint.before_each do |endpoint| + allow(endpoint).to receive(:client).and_return(client) + allow(client).to receive(:repo).with(project_key, repo_slug).and_return(double(name: repo_slug)) + end + end + + after do + Grape::Endpoint.before_each nil + end + + it 'returns 400 response due to a blcoked URL' do + allow(Gitlab::BitbucketServerImport::ProjectCreator) + .to receive(:new).with(project_key, repo_slug, anything, project.name, user.namespace, user, anything) + .and_return(double(execute: project)) + + allow(Gitlab::UrlBlocker) + .to receive(:blocked_url?) + .and_return(true) + post api("/import/bitbucket_server", user), params: { + bitbucket_server_url: base_uri, + bitbucket_server_username: user, + personal_access_token: token, + bitbucket_server_project: project_key, + bitbucket_server_repo: repo_slug, + new_name: 'new-name' + } + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'with a new namespace' do + let(:bitbucket_client) { instance_double(BitbucketServer::Client) } + + before do + Grape::Endpoint.before_each do |endpoint| + allow(endpoint).to receive(:client).and_return(bitbucket_client) + repo = double(name: repo_slug, full_path: "/other-namespace/#{repo_slug}") + allow(bitbucket_client).to receive(:repo).with(project_key, repo_slug).and_return(repo) + end + end + + after do + Grape::Endpoint.before_each nil + end + + it 'returns 201 response when the project is imported successfully to a new namespace' do + allow(Gitlab::BitbucketServerImport::ProjectCreator) + .to receive(:new).with(project_key, repo_slug, anything, repo_slug, an_instance_of(Group), user, anything) + .and_return(double(execute: create(:project, name: repo_slug))) + + post api("/import/bitbucket_server", user), params: { + bitbucket_server_url: base_uri, + bitbucket_server_username: user, + personal_access_token: token, + bitbucket_server_project: project_key, + bitbucket_server_repo: repo_slug, + new_namespace: 'new-namespace' + } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to be_a Hash + expect(json_response['full_path']).not_to eq("/#{user.namespace}/#{repo_slug}") + end + end + + context 'with a private inaccessible namespace' do + let(:bitbucket_client) { instance_double(BitbucketServer::Client) } + let(:project) { create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim', namespace: 'private-group/vim') } + + before do + Grape::Endpoint.before_each do |endpoint| + allow(endpoint).to receive(:client).and_return(bitbucket_client) + repo = double(name: repo_slug, full_path: "/private-group/#{repo_slug}") + allow(bitbucket_client).to receive(:repo).with(project_key, repo_slug).and_return(repo) + end + end + + after do + Grape::Endpoint.before_each nil + end + + it 'returns 401 response when user can not create projects in the chosen namespace' do + allow(Gitlab::BitbucketServerImport::ProjectCreator) + .to receive(:new).with(project_key, repo_slug, anything, repo_slug, an_instance_of(Group), user, anything) + .and_return(double(execute: build(:project))) + + other_namespace = create(:group, :private, name: 'private-group') + + post api("/import/bitbucket_server", user), params: { + bitbucket_server_url: base_uri, + bitbucket_server_username: user, + personal_access_token: token, + bitbucket_server_project: project_key, + bitbucket_server_repo: repo_slug, + new_namespace: other_namespace.name + } + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'with an inaccessible bitbucket server instance' do + let(:bitbucket_client) { instance_double(BitbucketServer::Client) } + let(:project) { create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim', namespace: 'private-group/vim') } + + before do + Grape::Endpoint.before_each do |endpoint| + allow(endpoint).to receive(:client).and_return(bitbucket_client) + allow(bitbucket_client).to receive(:repo).with(project_key, repo_slug).and_raise(::BitbucketServer::Connection::ConnectionError) + end + end + + after do + Grape::Endpoint.before_each nil + end + + it 'raises a connection error' do + post api("/import/bitbucket_server", user), params: { + bitbucket_server_url: base_uri, + bitbucket_server_username: user, + personal_access_token: token, + bitbucket_server_project: project_key, + bitbucket_server_repo: repo_slug, + new_namespace: 'new-namespace' + } + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end +end diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb index f33436df40e..f026314f7a8 100644 --- a/spec/requests/api/import_github_spec.rb +++ b/spec/requests/api/import_github_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::ImportGithub do +RSpec.describe API::ImportGithub do let(:token) { "asdasd12345" } let(:provider) { :github } let(:access_params) { { github_access_token: token } } @@ -26,6 +26,10 @@ describe API::ImportGithub do end end + after do + Grape::Endpoint.before_each nil + end + it 'rejects requests when Github Importer is disabled' do stub_application_setting(import_sources: nil) diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index aa5e2367a2b..7d219954e9d 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Internal::Base do +RSpec.describe API::Internal::Base do let_it_be(:user, reload: true) { create(:user) } let_it_be(:project, reload: true) { create(:project, :repository, :wiki_repo) } let_it_be(:personal_snippet) { create(:personal_snippet, :repository, author: user) } @@ -467,21 +467,6 @@ describe API::Internal::Base do expect(json_response["git_config_options"]).to include("uploadpack.allowFilter=true") expect(json_response["git_config_options"]).to include("uploadpack.allowAnySHA1InWant=true") end - - context 'when gitaly_upload_pack_filter feature flag is disabled' do - before do - stub_feature_flags(gitaly_upload_pack_filter: false) - end - - it 'returns only maxInputSize and not partial clone git config' do - push(key, project) - - expect(json_response["git_config_options"]).to be_present - expect(json_response["git_config_options"]).to include("receive.maxInputSize=1048576") - expect(json_response["git_config_options"]).not_to include("uploadpack.allowFilter=true") - expect(json_response["git_config_options"]).not_to include("uploadpack.allowAnySHA1InWant=true") - end - end end context 'when receive_max_input_size is empty' do @@ -496,18 +481,6 @@ describe API::Internal::Base do expect(json_response["git_config_options"]).to include("uploadpack.allowFilter=true") expect(json_response["git_config_options"]).to include("uploadpack.allowAnySHA1InWant=true") end - - context 'when gitaly_upload_pack_filter feature flag is disabled' do - before do - stub_feature_flags(gitaly_upload_pack_filter: false) - end - - it 'returns an empty git config' do - push(key, project) - - expect(json_response["git_config_options"]).to be_empty - end - end end end diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb index fecf15c29c2..48fc95b6574 100644 --- a/spec/requests/api/internal/pages_spec.rb +++ b/spec/requests/api/internal/pages_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Internal::Pages do +RSpec.describe API::Internal::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 } diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb index 5c925d2a32e..b53fac3679d 100644 --- a/spec/requests/api/issues/get_group_issues_spec.rb +++ b/spec/requests/api/issues/get_group_issues_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Issues do +RSpec.describe API::Issues do let_it_be(:user2) { create(:user) } let_it_be(:admin) { create(:user, :admin) } let_it_be(:non_member) { create(:user) } diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb index 4a728c81215..7ff07bf580d 100644 --- a/spec/requests/api/issues/get_project_issues_spec.rb +++ b/spec/requests/api/issues/get_project_issues_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Issues do +RSpec.describe API::Issues do let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace) } let_it_be(:private_mrs_project) do diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb index 315396c89c3..519bea22501 100644 --- a/spec/requests/api/issues/issues_spec.rb +++ b/spec/requests/api/issues/issues_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Issues do +RSpec.describe API::Issues do let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace) } let_it_be(:private_mrs_project) do @@ -886,4 +886,53 @@ describe API::Issues do include_examples 'time tracking endpoints', 'issue' end + + describe 'PUT /projects/:id/issues/:issue_iid/reorder' do + let_it_be(:project) { create(:project) } + let_it_be(:issue1) { create(:issue, project: project, relative_position: 10) } + let_it_be(:issue2) { create(:issue, project: project, relative_position: 20) } + let_it_be(:issue3) { create(:issue, project: project, relative_position: 30) } + + context 'when user has access' do + before do + project.add_developer(user) + end + + context 'with valid params' do + it 'reorders issues and returns a successful 200 response' 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) + expect(issue1.reload.relative_position) + .to be_between(issue2.reload.relative_position, issue3.reload.relative_position) + end + end + + context 'with invalid params' do + it 'returns a unprocessable entity 422 response for invalid move ids' do + put api("/projects/#{project.id}/issues/#{issue1.iid}/reorder", user), params: { move_after_id: issue2.id, move_before_id: non_existing_record_id } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + + it 'returns a not found 404 response for invalid issue id' do + put api("/projects/#{project.id}/issues/#{non_existing_record_iid}/reorder", user), params: { move_after_id: issue2.id, move_before_id: issue3.id } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'with unauthorized user' do + before do + project.add_guest(user) + end + + it 'responds with 403 forbidden' 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(:forbidden) + end + end + end end diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb index 2e1e5d3204e..e2f1bb2cd1a 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' -describe API::Issues do +RSpec.describe API::Issues do let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) do create(:project, :public, creator_id: user.id, namespace: user.namespace) diff --git a/spec/requests/api/issues/put_projects_issues_spec.rb b/spec/requests/api/issues/put_projects_issues_spec.rb index 62a4d3b48b2..dac721cbea0 100644 --- a/spec/requests/api/issues/put_projects_issues_spec.rb +++ b/spec/requests/api/issues/put_projects_issues_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Issues do +RSpec.describe API::Issues do let_it_be(:user) { create(:user) } let_it_be(:owner) { create(:owner) } let(:user2) { create(:user) } diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index 18b5c00d64f..53c57931d36 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Jobs do +RSpec.describe API::Jobs do include HttpIOHelpers shared_examples 'a job with artifacts and trace' do |result_is_array: true| @@ -36,9 +36,9 @@ describe API::Jobs do end let_it_be(:pipeline, reload: true) do - create(:ci_empty_pipeline, project: project, - sha: project.commit.id, - ref: project.default_branch) + create(:ci_pipeline, project: project, + sha: project.commit.id, + ref: project.default_branch) end let!(:job) do diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb index 089ee22982c..49b8f4a8520 100644 --- a/spec/requests/api/keys_spec.rb +++ b/spec/requests/api/keys_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Keys do +RSpec.describe API::Keys do let(:user) { create(:user) } let(:admin) { create(:admin) } let(:key) { create(:key, user: user, expires_at: 1.day.from_now) } diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 697f22e5f29..fc674fca9b2 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Labels do +RSpec.describe API::Labels do def put_labels_api(route_type, user, spec_params, request_params = {}) if route_type == :deprecated put api("/projects/#{project.id}/labels", user), diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb index 71c2619d898..4c60c8bd2a3 100644 --- a/spec/requests/api/lint_spec.rb +++ b/spec/requests/api/lint_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Lint do +RSpec.describe API::Lint do describe 'POST /ci/lint' do context 'with valid .gitlab-ci.yaml content' do let(:yaml_content) do diff --git a/spec/requests/api/markdown_spec.rb b/spec/requests/api/markdown_spec.rb index 53e43430b1f..35d91963ac9 100644 --- a/spec/requests/api/markdown_spec.rb +++ b/spec/requests/api/markdown_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe API::Markdown do +RSpec.describe API::Markdown do describe "POST /markdown" do let(:user) {} # No-op. It gets overwritten in the contexts below. diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb new file mode 100644 index 00000000000..189d6a4c1a4 --- /dev/null +++ b/spec/requests/api/maven_packages_spec.rb @@ -0,0 +1,569 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe API::MavenPackages do + include WorkhorseHelpers + + let_it_be(:group) { create(:group) } + let_it_be(:user) { create(:user) } + let_it_be(:project, reload: true) { create(:project, :public, namespace: group) } + let_it_be(:package, reload: true) { create(:maven_package, project: project, name: project.full_path) } + let_it_be(:maven_metadatum, reload: true) { package.maven_metadatum } + let_it_be(:package_file) { package.package_files.with_file_name_like('%.xml').first } + let_it_be(:jar_file) { package.package_files.with_file_name_like('%.jar').first } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:job) { create(:ci_build, user: user) } + 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(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } + let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } } + let(:headers_with_token) { headers.merge('Private-Token' => personal_access_token.token) } + + let(:headers_with_deploy_token) do + headers.merge( + Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token.token + ) + end + + let(:version) { '1.0-SNAPSHOT' } + + before do + project.add_developer(user) + end + + shared_examples 'tracking the file download event' do + context 'with jar file' do + let_it_be(:package_file) { jar_file } + + it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package' + end + end + + shared_examples 'processing HEAD requests' do + subject { head api(url) } + + before do + allow_any_instance_of(::Packages::PackageFileUploader).to receive(:fog_credentials).and_return(object_storage_credentials) + stub_package_file_object_storage(enabled: object_storage_enabled) + end + + context 'with object storage enabled' do + let(:object_storage_enabled) { true } + + before do + allow_any_instance_of(::Packages::PackageFileUploader).to receive(:file_storage?).and_return(false) + end + + context 'non AWS provider' do + let(:object_storage_credentials) { { provider: 'Google' } } + + it 'does not generated a signed url for head' do + expect_any_instance_of(Fog::AWS::Storage::Files).not_to receive(:head_url) + + subject + end + end + + context 'with AWS provider' do + let(:object_storage_credentials) { { provider: 'AWS', aws_access_key_id: 'test', aws_secret_access_key: 'test' } } + + it 'generates a signed url for head' do + expect_any_instance_of(Fog::AWS::Storage::Files).to receive(:head_url).and_call_original + + subject + end + end + end + + context 'with object storage disabled' do + let(:object_storage_enabled) { false } + let(:object_storage_credentials) { {} } + + it 'does not generate a signed url for head' do + expect_any_instance_of(Fog::AWS::Storage::Files).not_to receive(:head_url) + + subject + end + end + end + + shared_examples 'downloads with a deploy token' do + it 'allows download with deploy token' do + download_file( + package_file.file_name, + {}, + Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token.token + ) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + end + + shared_examples 'downloads with a job token' do + it 'allows download with job token' do + download_file(package_file.file_name, job_token: job.token) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + end + + describe 'GET /api/v4/packages/maven/*path/:file_name' do + context 'a public project' do + subject { download_file(package_file.file_name) } + + it_behaves_like 'tracking the file download event' + + it 'returns the file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + + it 'returns sha1 of the file' do + download_file(package_file.file_name + '.sha1') + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('text/plain') + expect(response.body).to eq(package_file.file_sha1) + end + end + + context 'internal project' do + before do + project.team.truncate + project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + end + + subject { download_file_with_token(package_file.file_name) } + + it_behaves_like 'tracking the file download event' + + it 'returns the file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + + it 'denies download when no private token' do + download_file(package_file.file_name) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it_behaves_like 'downloads with a job token' + + it_behaves_like 'downloads with a deploy token' + end + + context 'private project' do + subject { download_file_with_token(package_file.file_name) } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + it_behaves_like 'tracking the file download event' + + it 'returns the file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + + it 'denies download when not enough permissions' do + project.add_guest(user) + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'denies download when no private token' do + download_file(package_file.file_name) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it_behaves_like 'downloads with a job token' + + it_behaves_like 'downloads with a deploy token' + 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(package_file.file_name) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + def download_file(file_name, params = {}, request_headers = headers) + get api("/packages/maven/#{maven_metadatum.path}/#{file_name}"), params: params, headers: request_headers + end + + def download_file_with_token(file_name, params = {}, request_headers = headers_with_token) + download_file(file_name, params, request_headers) + end + end + + describe 'HEAD /api/v4/packages/maven/*path/:file_name' do + let(:url) { "/packages/maven/#{package.maven_metadatum.path}/#{package_file.file_name}" } + + it_behaves_like 'processing HEAD requests' + end + + describe 'GET /api/v4/groups/:id/-/packages/maven/*path/:file_name' do + before do + project.team.truncate + group.add_developer(user) + end + + context 'a public project' do + subject { download_file(package_file.file_name) } + + it_behaves_like 'tracking the file download event' + + it 'returns the file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + + it 'returns sha1 of the file' do + download_file(package_file.file_name + '.sha1') + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('text/plain') + expect(response.body).to eq(package_file.file_sha1) + end + end + + context 'internal project' do + before do + group.group_member(user).destroy + project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + end + + subject { download_file_with_token(package_file.file_name) } + + it_behaves_like 'tracking the file download event' + + it 'returns the file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + + it 'denies download when no private token' do + download_file(package_file.file_name) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it_behaves_like 'downloads with a job token' + + it_behaves_like 'downloads with a deploy token' + end + + context 'private project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + subject { download_file_with_token(package_file.file_name) } + + it_behaves_like 'tracking the file download event' + + it 'returns the file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + + it 'denies download when not enough permissions' do + group.add_guest(user) + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'denies download when no private token' do + download_file(package_file.file_name) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it_behaves_like 'downloads with a job token' + + it_behaves_like 'downloads with a deploy token' + end + + def download_file(file_name, params = {}, request_headers = headers) + get api("/groups/#{group.id}/-/packages/maven/#{maven_metadatum.path}/#{file_name}"), params: params, headers: request_headers + end + + def download_file_with_token(file_name, params = {}, request_headers = headers_with_token) + download_file(file_name, params, request_headers) + end + end + + describe 'HEAD /api/v4/groups/:id/-/packages/maven/*path/:file_name' do + let(:url) { "/groups/#{group.id}/-/packages/maven/#{package.maven_metadatum.path}/#{package_file.file_name}" } + + it_behaves_like 'processing HEAD requests' + end + + describe 'GET /api/v4/projects/:id/packages/maven/*path/:file_name' do + context 'a public project' do + subject { download_file(package_file.file_name) } + + it_behaves_like 'tracking the file download event' + + it 'returns the file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + + it 'returns sha1 of the file' do + download_file(package_file.file_name + '.sha1') + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('text/plain') + expect(response.body).to eq(package_file.file_sha1) + end + end + + context 'private project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + subject { download_file_with_token(package_file.file_name) } + + it_behaves_like 'tracking the file download event' + + it 'returns the file' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + + it 'denies download when not enough permissions' do + project.add_guest(user) + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'denies download when no private token' do + download_file(package_file.file_name) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it_behaves_like 'downloads with a job token' + + it_behaves_like 'downloads with a deploy token' + end + + def download_file(file_name, params = {}, request_headers = headers) + get api("/projects/#{project.id}/packages/maven/" \ + "#{maven_metadatum.path}/#{file_name}"), params: params, headers: request_headers + end + + def download_file_with_token(file_name, params = {}, request_headers = headers_with_token) + download_file(file_name, params, request_headers) + end + end + + describe 'HEAD /api/v4/projects/:id/packages/maven/*path/:file_name' do + let(:url) { "/projects/#{project.id}/packages/maven/#{package.maven_metadatum.path}/#{package_file.file_name}" } + + it_behaves_like 'processing HEAD requests' + end + + describe 'PUT /api/v4/projects/:id/packages/maven/*path/:file_name/authorize' do + it 'rejects a malicious request' do + put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/%2e%2e%2F.ssh%2Fauthorized_keys/authorize"), params: {}, headers: headers_with_token + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'authorizes posting package with a valid token' do + authorize_upload_with_token + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response['TempPath']).not_to be_nil + end + + it 'rejects request without a valid token' do + headers_with_token['Private-Token'] = 'foo' + + authorize_upload_with_token + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + it 'rejects request without a valid permission' do + project.add_guest(user) + + authorize_upload_with_token + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'rejects requests that did not go through gitlab-workhorse' do + headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) + + authorize_upload_with_token + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'authorizes upload with job token' do + authorize_upload(job_token: job.token) + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'authorizes upload with deploy token' do + authorize_upload({}, headers_with_deploy_token) + + expect(response).to have_gitlab_http_status(:ok) + end + + def authorize_upload(params = {}, request_headers = headers) + put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/maven-metadata.xml/authorize"), params: params, headers: request_headers + end + + def authorize_upload_with_token(params = {}, request_headers = headers_with_token) + authorize_upload(params, request_headers) + end + end + + describe 'PUT /api/v4/projects/:id/packages/maven/*path/:file_name' do + let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } + let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } } + let(:send_rewritten_field) { true } + let(:file_upload) { fixture_file_upload('spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.jar') } + + before do + # by configuring this path we allow to pass temp file from any path + allow(Packages::PackageFileUploader).to receive(:workhorse_upload_path).and_return('/') + end + + it 'rejects requests without a file from workhorse' do + upload_file_with_token + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'rejects request without a token' do + upload_file + + expect(response).to have_gitlab_http_status(:unauthorized) + end + + context 'without workhorse rewritten field' do + let(:send_rewritten_field) { false } + + it 'rejects the request' do + upload_file_with_token + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'when params from workhorse are correct' do + let(:params) { { file: file_upload } } + + it 'rejects a malicious request' do + put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/%2e%2e%2f.ssh%2fauthorized_keys"), params: params, headers: headers_with_token + + expect(response).to have_gitlab_http_status(:bad_request) + end + + context 'without workhorse header' do + let(:workhorse_header) { {} } + + subject { upload_file_with_token(params) } + + it_behaves_like 'package workhorse uploads' + end + + context 'event tracking' do + subject { upload_file_with_token(params) } + + it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package' + end + + it 'creates package and stores package file' do + expect { upload_file_with_token(params) }.to change { project.packages.count }.by(1) + .and change { Packages::Maven::Metadatum.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + + expect(response).to have_gitlab_http_status(:ok) + expect(jar_file.file_name).to eq(file_upload.original_filename) + end + + it 'allows upload with job token' do + upload_file(params.merge(job_token: job.token)) + + expect(response).to have_gitlab_http_status(:ok) + expect(project.reload.packages.last.build_info.pipeline).to eq job.pipeline + end + + it 'allows upload with deploy token' do + upload_file(params, headers_with_deploy_token) + + expect(response).to have_gitlab_http_status(:ok) + end + + context 'version is not correct' do + let(:version) { '$%123' } + + it 'rejects request' do + expect { upload_file_with_token(params) }.not_to change { project.packages.count } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to include('Validation failed') + end + end + end + + def upload_file(params = {}, request_headers = headers) + url = "/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/my-app-1.0-20180724.124855-1.jar" + workhorse_finalize( + api(url), + method: :put, + file_key: :file, + params: params, + headers: request_headers, + send_rewritten_field: send_rewritten_field + ) + end + + def upload_file_with_token(params = {}, request_headers = headers_with_token) + upload_file(params, request_headers) + end + end +end diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 0ecef26c27a..23889912d7a 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Members do +RSpec.describe API::Members do let(:maintainer) { create(:user, username: 'maintainer_user') } let(:developer) { create(:user) } let(:access_requester) { create(:user) } @@ -321,6 +321,26 @@ describe API::Members do expect(response).to have_gitlab_http_status(:bad_request) end end + + context 'adding project bot' do + let_it_be(:project_bot) { create(:user, :project_bot) } + + before do + unrelated_project = create(:project) + unrelated_project.add_maintainer(project_bot) + end + + it 'returns 400' do + expect do + post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), + params: { user_id: project_bot.id, access_level: Member::DEVELOPER } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']['user_id']).to( + include('project bots cannot be added to other groups / projects')) + end.not_to change { project.members.count } + end + end end shared_examples 'PUT /:source_type/:id/members/:user_id' do |source_type| @@ -461,8 +481,34 @@ describe API::Members do end end - it_behaves_like 'POST /:source_type/:id/members', 'project' do - let(:source) { project } + describe 'POST /projects/:id/members' do + it_behaves_like 'POST /:source_type/:id/members', 'project' do + let(:source) { project } + end + + context 'adding owner to project' do + it 'returns 403' do + expect do + post api("/projects/#{project.id}/members", maintainer), + params: { user_id: stranger.id, access_level: Member::OWNER } + + expect(response).to have_gitlab_http_status(:bad_request) + end.not_to change { project.members.count } + end + end + + context 'remove bot from project' do + it 'returns a 403 forbidden' do + project_bot = create(:user, :project_bot) + create(:project_member, project: project, user: project_bot) + + expect do + delete api("/projects/#{project.id}/members/#{project_bot.id}", maintainer) + + expect(response).to have_gitlab_http_status(:forbidden) + end.not_to change { project.members.count } + end + end end it_behaves_like 'POST /:source_type/:id/members', 'group' do @@ -484,15 +530,4 @@ describe API::Members do it_behaves_like 'DELETE /:source_type/:id/members/:user_id', 'group' do let(:source) { group } end - - context 'Adding owner to project' do - it 'returns 403' do - expect do - post api("/projects/#{project.id}/members", maintainer), - params: { user_id: stranger.id, access_level: Member::OWNER } - - expect(response).to have_gitlab_http_status(:bad_request) - end.to change { project.members.count }.by(0) - end - end end diff --git a/spec/requests/api/merge_request_approvals_spec.rb b/spec/requests/api/merge_request_approvals_spec.rb new file mode 100644 index 00000000000..fad5c3fb60e --- /dev/null +++ b/spec/requests/api/merge_request_approvals_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::MergeRequestApprovals do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) } + let_it_be(:approver) { create :user } + let_it_be(:group) { create :group } + + let(:merge_request) { create(:merge_request, :simple, author: user, source_project: project) } + + describe 'GET :id/merge_requests/:merge_request_iid/approvals' do + it 'retrieves the approval status' do + project.add_developer(approver) + project.add_developer(create(:user)) + + create(:approval, user: approver, merge_request: merge_request) + + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/approvals", user) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + describe 'POST :id/merge_requests/:merge_request_iid/approve' do + context 'as a valid approver' do + let_it_be(:approver) { create(:user) } + + before do + project.add_developer(approver) + project.add_developer(create(:user)) + end + + def approve(extra_params = {}) + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/approve", approver), params: extra_params + end + + context 'when the sha param is not set' do + it 'approves the merge request' do + approve + + expect(response).to have_gitlab_http_status(:created) + end + end + + context 'when the sha param is correct' do + it 'approves the merge request' do + approve(sha: merge_request.diff_head_sha) + + expect(response).to have_gitlab_http_status(:created) + end + end + + context 'when the sha param is incorrect' do + it 'does not approve the merge request' do + approve(sha: merge_request.diff_head_sha.reverse) + + expect(response).to have_gitlab_http_status(:conflict) + expect(merge_request.approvals).to be_empty + end + end + end + end + + describe 'POST :id/merge_requests/:merge_request_iid/unapprove' do + context 'as a user who has approved the merge request' do + it 'unapproves the merge request' do + unapprover = create(:user) + + project.add_developer(approver) + project.add_developer(unapprover) + project.add_developer(create(:user)) + + create(:approval, user: approver, merge_request: merge_request) + create(:approval, user: unapprover, merge_request: merge_request) + + post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unapprove", unapprover) + + expect(response).to have_gitlab_http_status(:created) + end + end + end +end diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb index d00bc4a6dde..3f41a7a034d 100644 --- a/spec/requests/api/merge_request_diffs_spec.rb +++ b/spec/requests/api/merge_request_diffs_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe API::MergeRequestDiffs, 'MergeRequestDiffs' do +RSpec.describe API::MergeRequestDiffs, 'MergeRequestDiffs' do let!(:user) { create(:user) } let!(:merge_request) { create(:merge_request, importing: true) } let!(:project) { merge_request.target_project } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 7a0077f853a..68f1a0f1ba1 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe API::MergeRequests do +RSpec.describe API::MergeRequests do include ProjectForksHelper let(:base_time) { Time.now } @@ -425,6 +425,73 @@ describe API::MergeRequests do end end + context 'NOT params' do + let(:merge_request2) do + create( + :merge_request, + :simple, + milestone: milestone, + author: user, + assignees: [user], + merge_request_context_commits: [merge_request_context_commit], + source_project: project, + target_project: project, + source_branch: 'what', + title: "What", + created_at: base_time + ) + end + + before do + create(:label_link, label: label, target: merge_request) + create(:label_link, label: label2, target: merge_request2) + end + + it 'returns merge requests without any of the labels given', :aggregate_failures do + get api(endpoint_path, user), params: { not: { labels: ["#{label.title}, #{label2.title}"] } } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(3) + json_response.each do |mr| + expect(mr['labels']).not_to include(label2.title, label.title) + end + end + + it 'returns merge requests without any of the milestones given', :aggregate_failures do + get api(endpoint_path, user), params: { not: { milestone: milestone.title } } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(4) + json_response.each do |mr| + expect(mr['milestone']).not_to eq(milestone.title) + end + end + + it 'returns merge requests without the author given', :aggregate_failures do + get api(endpoint_path, user), params: { not: { author_id: user2.id } } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(5) + json_response.each do |mr| + expect(mr['author']['id']).not_to eq(user2.id) + end + end + + it 'returns merge requests without the assignee given', :aggregate_failures do + get api(endpoint_path, user), params: { not: { assignee_id: user2.id } } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(5) + json_response.each do |mr| + expect(mr['assignee']['id']).not_to eq(user2.id) + end + end + end + context 'source_branch param' do it 'returns merge requests with the given source branch' do get api(endpoint_path, user), params: { source_branch: merge_request_closed.source_branch, state: 'all' } @@ -1930,7 +1997,7 @@ describe API::MergeRequests do it "updates the MR's squash attribute" do expect do put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), params: { squash: true } - end.to change { merge_request.reload.squash } + end.to change { merge_request.reload.squash_on_merge? } expect(response).to have_gitlab_http_status(:ok) end diff --git a/spec/requests/api/metrics/dashboard/annotations_spec.rb b/spec/requests/api/metrics/dashboard/annotations_spec.rb index 6377ef2435a..07de2925ee2 100644 --- a/spec/requests/api/metrics/dashboard/annotations_spec.rb +++ b/spec/requests/api/metrics/dashboard/annotations_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Metrics::Dashboard::Annotations do +RSpec.describe API::Metrics::Dashboard::Annotations do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :private, :repository, namespace: user.namespace) } let_it_be(:environment) { create(:environment, project: project) } diff --git a/spec/requests/api/metrics/user_starred_dashboards_spec.rb b/spec/requests/api/metrics/user_starred_dashboards_spec.rb index 8f9394a0e20..533dff05f27 100644 --- a/spec/requests/api/metrics/user_starred_dashboards_spec.rb +++ b/spec/requests/api/metrics/user_starred_dashboards_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Metrics::UserStarredDashboards do +RSpec.describe API::Metrics::UserStarredDashboards do let_it_be(:user) { create(:user) } let_it_be(:dashboard_yml) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') } let_it_be(:dashboard) { '.gitlab/dashboards/find&seek.yml' } diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index 68fffc638df..2ac76d469d5 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Namespaces do +RSpec.describe API::Namespaces do let(:admin) { create(:admin) } let(:user) { create(:user) } let!(:group1) { create(:group, name: 'group.one') } diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 797dd3bb4e2..1510d31a1a6 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Notes do +RSpec.describe API::Notes do let!(:user) { create(:user) } let!(:project) { create(:project, :public) } let(:private_user) { create(:user) } diff --git a/spec/requests/api/notification_settings_spec.rb b/spec/requests/api/notification_settings_spec.rb index 2dfde4c8ec9..73cb4948524 100644 --- a/spec/requests/api/notification_settings_spec.rb +++ b/spec/requests/api/notification_settings_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::NotificationSettings do +RSpec.describe API::NotificationSettings do let(:user) { create(:user) } let!(:group) { create(:group) } let!(:project) { create(:project, :public, creator_id: user.id, namespace: group) } diff --git a/spec/requests/api/npm_packages_spec.rb b/spec/requests/api/npm_packages_spec.rb new file mode 100644 index 00000000000..98a1ca978a8 --- /dev/null +++ b/spec/requests/api/npm_packages_spec.rb @@ -0,0 +1,550 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::NpmPackages do + include PackagesManagerApiSpecHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project, reload: true) { create(:project, :public, namespace: group) } + let_it_be(:package, reload: true) { create(:npm_package, project: project) } + let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:job) { create(:ci_build, user: user) } + 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) } + + before do + project.add_developer(user) + end + + shared_examples 'a package that requires auth' do + it 'returns the package info with oauth token' do + get_package_with_token(package) + + expect_a_valid_package_response + end + + it 'returns the package info with job token' do + get_package_with_job_token(package) + + expect_a_valid_package_response + end + + it 'denies request without oauth token' do + get_package(package) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'returns the package info with deploy token' do + get_package_with_deploy_token(package) + + expect_a_valid_package_response + end + end + + describe 'GET /api/v4/packages/npm/*package_name' do + let_it_be(:package_dependency_link1) { create(:packages_dependency_link, package: package, dependency_type: :dependencies) } + let_it_be(:package_dependency_link2) { create(:packages_dependency_link, package: package, dependency_type: :devDependencies) } + let_it_be(:package_dependency_link3) { create(:packages_dependency_link, package: package, dependency_type: :bundleDependencies) } + let_it_be(:package_dependency_link4) { create(:packages_dependency_link, package: package, dependency_type: :peerDependencies) } + + shared_examples 'returning the npm package info' do + it 'returns the package info' do + get_package(package) + + expect_a_valid_package_response + end + end + + shared_examples 'returning forbidden for unknown package' do + context 'with an unknown package' do + it 'returns forbidden' do + get api("/packages/npm/unknown") + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + context 'a public project' do + it_behaves_like 'returning the npm package info' + + context 'with application setting enabled' do + before do + stub_application_setting(npm_package_requests_forwarding: true) + end + + it_behaves_like 'returning the npm package info' + + context 'with unknown package' do + it 'returns a redirect' do + get api("/packages/npm/unknown") + + expect(response).to have_gitlab_http_status(:found) + expect(response.headers['Location']).to eq('https://registry.npmjs.org/unknown') + end + end + end + + context 'with application setting disabled' do + before do + stub_application_setting(npm_package_requests_forwarding: false) + end + + it_behaves_like 'returning the npm package info' + + it_behaves_like 'returning forbidden for unknown package' + end + + context 'project path with a dot' do + before do + project.update!(path: 'foo.bar') + end + + it_behaves_like 'returning the npm package info' + end + end + + context 'internal project' do + before do + project.team.truncate + project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + end + + it_behaves_like 'a package that requires auth' + end + + context 'private project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + it_behaves_like 'a package that requires auth' + + it 'denies request when not enough permissions' do + project.add_guest(user) + + get_package_with_token(package) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + def get_package(package, params = {}, headers = {}) + get api("/packages/npm/#{package.name}"), params: params, headers: headers + end + + def get_package_with_token(package, params = {}) + get_package(package, params.merge(access_token: token.token)) + end + + def get_package_with_job_token(package, params = {}) + get_package(package, params.merge(job_token: job.token)) + end + + def get_package_with_deploy_token(package, params = {}) + get_package(package, {}, build_token_auth_header(deploy_token.token)) + end + end + + describe 'GET /api/v4/projects/:id/packages/npm/*package_name/-/*file_name' do + let_it_be(:package_file) { package.package_files.first } + + shared_examples 'a package file that requires auth' do + it 'returns the file with an access token' do + get_file_with_token(package_file) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + + it 'returns the file with a job token' do + get_file_with_job_token(package_file) + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + + it 'denies download with no token' do + get_file(package_file) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'a public project' do + subject { get_file(package_file) } + + it 'returns the file with no token needed' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + + it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package' + end + + context 'private project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + it_behaves_like 'a package file that requires auth' + + it 'denies download when not enough permissions' do + project.add_guest(user) + + get_file_with_token(package_file) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'internal project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + end + + it_behaves_like 'a package file that requires auth' + end + + def get_file(package_file, params = {}) + get api("/projects/#{project.id}/packages/npm/" \ + "#{package_file.package.name}/-/#{package_file.file_name}"), params: params + end + + def get_file_with_token(package_file, params = {}) + get_file(package_file, params.merge(access_token: token.token)) + end + + def get_file_with_job_token(package_file, params = {}) + get_file(package_file, params.merge(job_token: job.token)) + end + end + + describe 'PUT /api/v4/projects/:id/packages/npm/:package_name' do + RSpec.shared_examples 'handling invalid record with 400 error' do + it 'handles an ActiveRecord::RecordInvalid exception with 400 error' do + expect { upload_package_with_token(package_name, params) } + .not_to change { project.packages.count } + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'when params are correct' do + context 'invalid package record' do + context 'unscoped package' do + let(:package_name) { 'my_unscoped_package' } + let(:params) { upload_params(package_name: package_name) } + + it_behaves_like 'handling invalid record with 400 error' + + context 'with empty versions' do + let(:params) { upload_params(package_name: package_name).merge!(versions: {}) } + + it 'throws a 400 error' do + expect { upload_package_with_token(package_name, params) } + .not_to change { project.packages.count } + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end + + context 'invalid package name' do + let(:package_name) { "@#{group.path}/my_inv@@lid_package_name" } + let(:params) { upload_params(package_name: package_name) } + + it_behaves_like 'handling invalid record with 400 error' + end + + context 'invalid package version' do + using RSpec::Parameterized::TableSyntax + + let(:package_name) { "@#{group.path}/my_package_name" } + + where(:version) do + [ + '1', + '1.2', + '1./2.3', + '../../../../../1.2.3', + '%2e%2e%2f1.2.3' + ] + end + + with_them do + let(:params) { upload_params(package_name: package_name, package_version: version) } + + it_behaves_like 'handling invalid record with 400 error' + end + end + end + + context 'scoped package' do + let(:package_name) { "@#{group.path}/my_package_name" } + let(:params) { upload_params(package_name: package_name) } + + context 'with access token' do + subject { upload_package_with_token(package_name, params) } + + it_behaves_like 'a gitlab tracking event', described_class.name, '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) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + it 'creates npm package with file with job token' do + expect { upload_package_with_job_token(package_name, params) } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + + expect(response).to have_gitlab_http_status(:ok) + end + + context 'with an authenticated job token' do + let!(:job) { create(:ci_build, user: user) } + + before do + Grape::Endpoint.before_each do |endpoint| + expect(endpoint).to receive(:current_authenticated_job) { job } + end + end + + after do + Grape::Endpoint.before_each nil + end + + it 'creates the package metadata' do + upload_package_with_token(package_name, params) + + expect(response).to have_gitlab_http_status(:ok) + expect(project.reload.packages.find(json_response['id']).build_info.pipeline).to eq job.pipeline + end + end + end + + context 'package creation fails' do + let(:package_name) { "@#{group.path}/my_package_name" } + let(:params) { upload_params(package_name: package_name) } + + it 'returns an error if the package already exists' do + create(:npm_package, project: project, version: '1.0.1', name: "@#{group.path}/my_package_name") + expect { upload_package_with_token(package_name, params) } + .not_to change { project.packages.count } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'with dependencies' do + let(:package_name) { "@#{group.path}/my_package_name" } + let(:params) { upload_params(package_name: package_name, file: 'npm/payload_with_duplicated_packages.json') } + + it 'creates npm package with file and dependencies' do + expect { upload_package_with_token(package_name, params) } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + .and change { Packages::Dependency.count}.by(4) + .and change { Packages::DependencyLink.count}.by(6) + + expect(response).to have_gitlab_http_status(:ok) + end + + context 'with existing dependencies' do + before do + name = "@#{group.path}/existing_package" + upload_package_with_token(name, upload_params(package_name: name, file: 'npm/payload_with_duplicated_packages.json')) + end + + it 'reuses them' do + expect { upload_package_with_token(package_name, params) } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + .and not_change { Packages::Dependency.count} + .and change { Packages::DependencyLink.count}.by(6) + end + end + end + end + + def upload_package(package_name, params = {}) + put api("/projects/#{project.id}/packages/npm/#{package_name.sub('/', '%2f')}"), params: params + end + + def upload_package_with_token(package_name, params = {}) + upload_package(package_name, params.merge(access_token: token.token)) + end + + def upload_package_with_job_token(package_name, params = {}) + upload_package(package_name, params.merge(job_token: job.token)) + end + + def upload_params(package_name:, package_version: '1.0.1', file: 'npm/payload.json') + Gitlab::Json.parse(fixture_file("packages/#{file}") + .gsub('@root/npm-test', package_name) + .gsub('1.0.1', package_version)) + end + end + + describe 'GET /api/v4/packages/npm/-/package/*package_name/dist-tags' do + let_it_be(:package_tag1) { create(:packages_tag, package: package) } + let_it_be(:package_tag2) { create(:packages_tag, package: package) } + + let(:package_name) { package.name } + let(:url) { "/packages/npm/-/package/#{package_name}/dist-tags" } + + subject { get api(url) } + + context 'without the need for a license' do + context 'with public project' do + context 'with authenticated user' do + subject { get api(url, personal_access_token: personal_access_token) } + + it_behaves_like 'returns package tags', :maintainer + it_behaves_like 'returns package tags', :developer + it_behaves_like 'returns package tags', :reporter + it_behaves_like 'returns package tags', :guest + end + + context 'with unauthenticated user' do + it_behaves_like 'returns package tags', :no_type + end + end + + context 'with private project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + context 'with authenticated user' do + subject { get api(url, personal_access_token: personal_access_token) } + + it_behaves_like 'returns package tags', :maintainer + it_behaves_like 'returns package tags', :developer + it_behaves_like 'returns package tags', :reporter + it_behaves_like 'rejects package tags access', :guest, :forbidden + end + + context 'with unauthenticated user' do + it_behaves_like 'rejects package tags access', :no_type, :forbidden + end + end + end + end + + describe 'PUT /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do + let_it_be(:tag_name) { 'test' } + + let(:package_name) { package.name } + let(:version) { package.version } + let(:url) { "/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}" } + + subject { put api(url), env: { 'api.request.body': version } } + + context 'without the need for a license' do + context 'with public project' do + context 'with authenticated user' do + subject { put api(url, personal_access_token: personal_access_token), env: { 'api.request.body': version } } + + it_behaves_like 'create package tag', :maintainer + it_behaves_like 'create package tag', :developer + it_behaves_like 'rejects package tags access', :reporter, :forbidden + it_behaves_like 'rejects package tags access', :guest, :forbidden + end + + context 'with unauthenticated user' do + it_behaves_like 'rejects package tags access', :no_type, :unauthorized + end + end + + context 'with private project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + context 'with authenticated user' do + subject { put api(url, personal_access_token: personal_access_token), env: { 'api.request.body': version } } + + it_behaves_like 'create package tag', :maintainer + it_behaves_like 'create package tag', :developer + it_behaves_like 'rejects package tags access', :reporter, :forbidden + it_behaves_like 'rejects package tags access', :guest, :forbidden + end + + context 'with unauthenticated user' do + it_behaves_like 'rejects package tags access', :no_type, :unauthorized + end + end + end + end + + describe 'DELETE /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do + let_it_be(:package_tag) { create(:packages_tag, package: package) } + + let(:package_name) { package.name } + let(:tag_name) { package_tag.name } + let(:url) { "/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}" } + + subject { delete api(url) } + + context 'without the need for a license' do + context 'with public project' do + context 'with authenticated user' do + subject { delete api(url, personal_access_token: personal_access_token) } + + it_behaves_like 'delete package tag', :maintainer + it_behaves_like 'rejects package tags access', :developer, :forbidden + it_behaves_like 'rejects package tags access', :reporter, :forbidden + it_behaves_like 'rejects package tags access', :guest, :forbidden + end + + context 'with unauthenticated user' do + it_behaves_like 'rejects package tags access', :no_type, :unauthorized + end + end + + context 'with private project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + context 'with authenticated user' do + subject { delete api(url, personal_access_token: personal_access_token) } + + it_behaves_like 'delete package tag', :maintainer + it_behaves_like 'rejects package tags access', :developer, :forbidden + it_behaves_like 'rejects package tags access', :reporter, :forbidden + it_behaves_like 'rejects package tags access', :guest, :forbidden + end + + context 'with unauthenticated user' do + it_behaves_like 'rejects package tags access', :no_type, :unauthorized + end + end + end + end + + def expect_a_valid_package_response + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/json') + expect(response).to match_response_schema('public_api/v4/packages/npm_package') + expect(json_response['name']).to eq(package.name) + expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version') + ::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type| + expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any + end + expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags') + end +end diff --git a/spec/requests/api/nuget_packages_spec.rb b/spec/requests/api/nuget_packages_spec.rb new file mode 100644 index 00000000000..43aa65d1f76 --- /dev/null +++ b/spec/requests/api/nuget_packages_spec.rb @@ -0,0 +1,482 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe API::NugetPackages do + include WorkhorseHelpers + include PackagesManagerApiSpecHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project, reload: true) { create(:project, :public) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } + 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) } + + describe 'GET /api/v4/projects/:id/packages/nuget' do + let(:url) { "/projects/#{project.id}/packages/nuget/index.json" } + + subject { get api(url) } + + context 'without the need for a license' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success + 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success + 'PUBLIC' | :developer | true | false | 'process nuget service index request' | :success + 'PUBLIC' | :guest | true | false | 'process nuget service index request' | :success + 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success + 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success + 'PUBLIC' | :developer | false | false | 'process nuget service index request' | :success + 'PUBLIC' | :guest | false | false | 'process nuget service index request' | :success + 'PUBLIC' | :anonymous | false | true | 'process nuget service index request' | :success + 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + it_behaves_like 'deploy token for package GET requests' + + it_behaves_like 'rejects nuget access with unknown project id' + + it_behaves_like 'rejects nuget access with invalid project id' + end + end + + describe 'PUT /api/v4/projects/:id/packages/nuget/authorize' do + let_it_be(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } + let_it_be(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } } + let(:url) { "/projects/#{project.id}/packages/nuget/authorize" } + let(:headers) { {} } + + subject { put api(url), headers: headers } + + context 'without the need for a license' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget workhorse authorization' | :success + 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process nuget workhorse authorization' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:user_headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + let(:headers) { user_headers.merge(workhorse_header) } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + it_behaves_like 'deploy token for package uploads' + + it_behaves_like 'rejects nuget access with unknown project id' + + it_behaves_like 'rejects nuget access with invalid project id' + end + end + + describe 'PUT /api/v4/projects/:id/packages/nuget' do + let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } + let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } } + let_it_be(:file_name) { 'package.nupkg' } + let(:url) { "/projects/#{project.id}/packages/nuget" } + let(:headers) { {} } + let(:params) { { package: temp_file(file_name) } } + let(:file_key) { :package } + let(:send_rewritten_field) { true } + + subject do + workhorse_finalize( + api(url), + method: :put, + file_key: file_key, + params: params, + headers: headers, + send_rewritten_field: send_rewritten_field + ) + end + + context 'without the need for a license' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget upload' | :created + 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process nuget upload' | :created + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:user_headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + let(:headers) { user_headers.merge(workhorse_header) } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + it_behaves_like 'deploy token for package uploads' + + it_behaves_like 'rejects nuget access with unknown project id' + + it_behaves_like 'rejects nuget access with invalid project id' + end + end + + describe 'GET /api/v4/projects/:id/packages/nuget/metadata/*package_name/index' do + include_context 'with expected presenters dependency groups' + + let_it_be(:package_name) { 'Dummy.Package' } + let_it_be(:packages) { create_list(:nuget_package, 5, :with_metadatum, name: package_name, project: project) } + let_it_be(:tags) { packages.each { |pkg| create(:packages_tag, package: pkg, name: 'test') } } + let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/index.json" } + + subject { get api(url) } + + before do + packages.each { |pkg| create_dependencies_for(pkg) } + end + + context 'without the need for license' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :guest | true | true | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :developer | true | false | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :guest | true | false | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :developer | false | true | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :developer | false | false | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :guest | false | false | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :anonymous | false | true | 'process nuget metadata request at package name level' | :success + 'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name level' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + + it_behaves_like 'deploy token for package GET requests' + + it_behaves_like 'rejects nuget access with unknown project id' + + it_behaves_like 'rejects nuget access with invalid project id' + end + end + end + + describe 'GET /api/v4/projects/:id/packages/nuget/metadata/*package_name/*package_version' do + include_context 'with expected presenters dependency groups' + + let_it_be(:package_name) { 'Dummy.Package' } + let_it_be(:package) { create(:nuget_package, :with_metadatum, name: 'Dummy.Package', project: project) } + let_it_be(:tag) { create(:packages_tag, package: package, name: 'test') } + let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/#{package.version}.json" } + + subject { get api(url) } + + before do + create_dependencies_for(package) + end + + context 'without the need for a license' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :guest | true | true | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :developer | true | false | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :guest | true | false | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :developer | false | true | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :developer | false | false | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :guest | false | false | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :anonymous | false | true | 'process nuget metadata request at package name and package version level' | :success + 'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + it_behaves_like 'deploy token for package GET requests' + + context 'with invalid package name' do + let_it_be(:package_name) { 'Unkown' } + + it_behaves_like 'rejects nuget packages access', :developer, :not_found + end + end + end + + describe 'GET /api/v4/projects/:id/packages/nuget/download/*package_name/index' do + let_it_be(:package_name) { 'Dummy.Package' } + let_it_be(:packages) { create_list(:nuget_package, 5, name: package_name, project: project) } + let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package_name}/index.json" } + + subject { get api(url) } + + context 'without the need for a license' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget download versions request' | :success + 'PUBLIC' | :guest | true | true | 'process nuget download versions request' | :success + 'PUBLIC' | :developer | true | false | 'process nuget download versions request' | :success + 'PUBLIC' | :guest | true | false | 'process nuget download versions request' | :success + 'PUBLIC' | :developer | false | true | 'process nuget download versions request' | :success + 'PUBLIC' | :guest | false | true | 'process nuget download versions request' | :success + 'PUBLIC' | :developer | false | false | 'process nuget download versions request' | :success + 'PUBLIC' | :guest | false | false | 'process nuget download versions request' | :success + 'PUBLIC' | :anonymous | false | true | 'process nuget download versions request' | :success + 'PRIVATE' | :developer | true | true | 'process nuget download versions request' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + it_behaves_like 'deploy token for package GET requests' + + it_behaves_like 'rejects nuget access with unknown project id' + + it_behaves_like 'rejects nuget access with invalid project id' + end + end + + describe 'GET /api/v4/projects/:id/packages/nuget/download/*package_name/*package_version/*package_filename' do + let_it_be(:package_name) { 'Dummy.Package' } + let_it_be(:package) { create(:nuget_package, project: project, name: package_name) } + + let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.name}.#{package.version}.nupkg" } + + subject { get api(url) } + + context 'without the need for a license' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget download content request' | :success + 'PUBLIC' | :guest | true | true | 'process nuget download content request' | :success + 'PUBLIC' | :developer | true | false | 'process nuget download content request' | :success + 'PUBLIC' | :guest | true | false | 'process nuget download content request' | :success + 'PUBLIC' | :developer | false | true | 'process nuget download content request' | :success + 'PUBLIC' | :guest | false | true | 'process nuget download content request' | :success + 'PUBLIC' | :developer | false | false | 'process nuget download content request' | :success + 'PUBLIC' | :guest | false | false | 'process nuget download content request' | :success + 'PUBLIC' | :anonymous | false | true | 'process nuget download content request' | :success + 'PRIVATE' | :developer | true | true | 'process nuget download content request' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + it_behaves_like 'deploy token for package GET requests' + + it_behaves_like 'rejects nuget access with unknown project id' + + it_behaves_like 'rejects nuget access with invalid project id' + end + end + + describe 'GET /api/v4/projects/:id/packages/nuget/query' do + let_it_be(:package_a) { create(:nuget_package, :with_metadatum, name: 'Dummy.PackageA', project: project) } + let_it_be(:tag) { create(:packages_tag, package: package_a, name: 'test') } + let_it_be(:packages_b) { create_list(:nuget_package, 5, name: 'Dummy.PackageB', project: project) } + let_it_be(:packages_c) { create_list(:nuget_package, 5, name: 'Dummy.PackageC', project: project) } + let_it_be(:package_d) { create(:nuget_package, name: 'Dummy.PackageD', version: '5.0.5-alpha', project: project) } + let_it_be(:package_e) { create(:nuget_package, name: 'Foo.BarE', project: project) } + let(:search_term) { 'uMmy' } + let(:take) { 26 } + let(:skip) { 0 } + let(:include_prereleases) { true } + let(:query_parameters) { { q: search_term, take: take, skip: skip, prerelease: include_prereleases } } + let(:url) { "/projects/#{project.id}/packages/nuget/query?#{query_parameters.to_query}" } + + subject { get api(url) } + + context 'without the need for a license' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget search request' | :success + 'PUBLIC' | :guest | true | true | 'process nuget search request' | :success + 'PUBLIC' | :developer | true | false | 'process nuget search request' | :success + 'PUBLIC' | :guest | true | false | 'process nuget search request' | :success + 'PUBLIC' | :developer | false | true | 'process nuget search request' | :success + 'PUBLIC' | :guest | false | true | 'process nuget search request' | :success + 'PUBLIC' | :developer | false | false | 'process nuget search request' | :success + 'PUBLIC' | :guest | false | false | 'process nuget search request' | :success + 'PUBLIC' | :anonymous | false | true | 'process nuget search request' | :success + 'PRIVATE' | :developer | true | true | 'process nuget search request' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + it_behaves_like 'deploy token for package GET requests' + + it_behaves_like 'rejects nuget access with unknown project id' + + it_behaves_like 'rejects nuget access with invalid project id' + end + end +end diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb index 5e775841f12..f5971054b3c 100644 --- a/spec/requests/api/oauth_tokens_spec.rb +++ b/spec/requests/api/oauth_tokens_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'OAuth tokens' do +RSpec.describe 'OAuth tokens' do include HttpBasicAuthHelpers context 'Resource Owner Password Credentials' do diff --git a/spec/requests/api/package_files_spec.rb b/spec/requests/api/package_files_spec.rb new file mode 100644 index 00000000000..11170066d6e --- /dev/null +++ b/spec/requests/api/package_files_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::PackageFiles do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:package) { create(:maven_package, project: project) } + + before do + project.add_developer(user) + end + + describe 'GET /projects/:id/packages/:package_id/package_files' do + let(:url) { "/projects/#{project.id}/packages/#{package.id}/package_files" } + + context 'without the need for a license' do + context 'project is public' do + it 'returns 200' do + get api(url) + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'returns 404 if package does not exist' do + get api("/projects/#{project.id}/packages/0/package_files") + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'project is private' do + let(:project) { create(:project, :private) } + + it 'returns 404 for non authenticated user' do + get api(url) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 404 for a user without access to the project' do + project.team.truncate + + get api(url, user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 200 and valid response schema' do + get api(url, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/packages/package_files') + end + end + + context 'with pagination params' do + let(:per_page) { 2 } + let!(:package_file_1) { package.package_files[0] } + let!(:package_file_2) { package.package_files[1] } + let!(:package_file_3) { package.package_files[2] } + + context 'when viewing the first page' do + it 'returns first 2 packages' do + get api(url, user), params: { page: 1, per_page: per_page } + + expect_paginated_array_response([package_file_1.id, package_file_2.id]) + end + end + + context 'viewing the second page' do + it 'returns the last package' do + get api(url, user), params: { page: 2, per_page: per_page } + + expect_paginated_array_response([package_file_3.id]) + end + 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 ee55d1c54b7..c894a2d3ca4 100644 --- a/spec/requests/api/pages/internal_access_spec.rb +++ b/spec/requests/api/pages/internal_access_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe "Internal Project Pages Access" do +RSpec.describe "Internal Project Pages Access" do using RSpec::Parameterized::TableSyntax include AccessMatchers diff --git a/spec/requests/api/pages/pages_spec.rb b/spec/requests/api/pages/pages_spec.rb index 62d43ecff16..53e732928ff 100644 --- a/spec/requests/api/pages/pages_spec.rb +++ b/spec/requests/api/pages/pages_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Pages do +RSpec.describe API::Pages do let_it_be(:project) { create(:project, path: 'my.project', pages_https_only: false) } let_it_be(:admin) { create(:admin) } let_it_be(:user) { create(:user) } diff --git a/spec/requests/api/pages/private_access_spec.rb b/spec/requests/api/pages/private_access_spec.rb index 146c6a389f3..ea5db691b14 100644 --- a/spec/requests/api/pages/private_access_spec.rb +++ b/spec/requests/api/pages/private_access_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe "Private Project Pages Access" do +RSpec.describe "Private Project Pages Access" do using RSpec::Parameterized::TableSyntax include AccessMatchers diff --git a/spec/requests/api/pages/public_access_spec.rb b/spec/requests/api/pages/public_access_spec.rb index 7d929e2a287..ae73cee91d5 100644 --- a/spec/requests/api/pages/public_access_spec.rb +++ b/spec/requests/api/pages/public_access_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe "Public Project Pages Access" do +RSpec.describe "Public Project Pages Access" do using RSpec::Parameterized::TableSyntax include AccessMatchers diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb index 8c411233b27..b6838a39257 100644 --- a/spec/requests/api/pages_domains_spec.rb +++ b/spec/requests/api/pages_domains_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::PagesDomains do +RSpec.describe API::PagesDomains do let_it_be(:project) { create(:project, path: 'my.project', pages_https_only: false) } let_it_be(:user) { create(:user) } let_it_be(:admin) { create(:admin) } diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb index ed899e830e1..ff35e380476 100644 --- a/spec/requests/api/project_clusters_spec.rb +++ b/spec/requests/api/project_clusters_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::ProjectClusters do +RSpec.describe API::ProjectClusters do include KubernetesHelpers let_it_be(:current_user) { create(:user) } @@ -40,7 +40,7 @@ describe API::ProjectClusters do expect(response).to include_pagination_headers end - it 'onlies include authorized clusters' do + it 'only includes authorized clusters' do cluster_ids = json_response.map { |cluster| cluster['id'] } expect(response).to have_gitlab_http_status(:ok) @@ -258,29 +258,52 @@ describe API::ProjectClusters do end end - context 'when user tries to add multiple clusters' do + context 'non-authorized user' do before do - create(:cluster, :provided_by_gcp, :project, - projects: [project]) - - post api("/projects/#{project.id}/clusters/user", current_user), params: cluster_params + post api("/projects/#{project.id}/clusters/user", developer_user), params: cluster_params end - it 'responds with 400' do - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']['base'].first) - .to eq(_('Instance does not support multiple Kubernetes clusters')) + it 'responds with 403' do + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['message']).to eq('403 Forbidden') end end + end - context 'non-authorized user' do + describe 'POST /projects/:id/clusters/user with multiple clusters' do + let(:api_url) { 'https://kubernetes.example.com' } + let(:namespace) { project.path } + + let(:platform_kubernetes_attributes) do + { + api_url: api_url, + token: 'sample-token', + namespace: namespace + } + end + + let(:cluster_params) do + { + name: 'test-cluster', + environment_scope: 'production/*', + platform_kubernetes_attributes: platform_kubernetes_attributes + } + end + + context 'when another cluster exists' do before do - post api("/projects/#{project.id}/clusters/user", developer_user), params: cluster_params + create(:cluster, :provided_by_gcp, :project, + projects: [project]) + + post api("/projects/#{project.id}/clusters/user", current_user), params: cluster_params end - it 'responds with 403' do - expect(response).to have_gitlab_http_status(:forbidden) - expect(json_response['message']).to eq('403 Forbidden') + it 'responds with 201' do + expect(response).to have_gitlab_http_status(:created) + end + + it 'allows multiple clusters to be associated to project' do + expect(project.reload.clusters.count).to eq(2) end end end diff --git a/spec/requests/api/project_container_repositories_spec.rb b/spec/requests/api/project_container_repositories_spec.rb index 471fc99117b..6cf0619cde4 100644 --- a/spec/requests/api/project_container_repositories_spec.rb +++ b/spec/requests/api/project_container_repositories_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::ProjectContainerRepositories do +RSpec.describe API::ProjectContainerRepositories do include ExclusiveLeaseHelpers let_it_be(:project) { create(:project, :private) } diff --git a/spec/requests/api/project_events_spec.rb b/spec/requests/api/project_events_spec.rb index f65c62f9402..f3e592f9796 100644 --- a/spec/requests/api/project_events_spec.rb +++ b/spec/requests/api/project_events_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::ProjectEvents do +RSpec.describe API::ProjectEvents do let(:user) { create(:user) } let(:non_member) { create(:user) } let(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) } diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb index 58034322a13..d7ba3b4e158 100644 --- a/spec/requests/api/project_export_spec.rb +++ b/spec/requests/api/project_export_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::ProjectExport, :clean_gitlab_redis_cache do +RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache do let_it_be(:project) { create(:project) } let_it_be(:project_none) { create(:project) } let_it_be(:project_started) { create(:project) } @@ -237,7 +237,7 @@ describe API::ProjectExport, :clean_gitlab_redis_cache do before do allow(Gitlab::ApplicationRateLimiter) .to receive(:increment) - .and_return(Gitlab::ApplicationRateLimiter.rate_limits[:project_download_export][:threshold] + 1) + .and_return(Gitlab::ApplicationRateLimiter.rate_limits[:project_download_export][:threshold].call + 1) end it 'prevents requesting project export' do @@ -362,7 +362,7 @@ describe API::ProjectExport, :clean_gitlab_redis_cache do before do allow(Gitlab::ApplicationRateLimiter) .to receive(:increment) - .and_return(Gitlab::ApplicationRateLimiter.rate_limits[:project_export][:threshold] + 1) + .and_return(Gitlab::ApplicationRateLimiter.rate_limits[:project_export][:threshold].call + 1) end it 'prevents requesting project export' do diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index 4474f2f0577..8ab90e26a51 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::ProjectHooks, 'ProjectHooks' do +RSpec.describe API::ProjectHooks, 'ProjectHooks' do let(:user) { create(:user) } let(:user3) { create(:user) } let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index 563acd0ece4..a6ae636996e 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::ProjectImport do +RSpec.describe API::ProjectImport do include WorkhorseHelpers let(:user) { create(:user) } diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb index c5911d51706..b238949ce47 100644 --- a/spec/requests/api/project_milestones_spec.rb +++ b/spec/requests/api/project_milestones_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::ProjectMilestones do +RSpec.describe API::ProjectMilestones do let(:user) { create(:user) } let!(:project) { create(:project, namespace: user.namespace ) } let!(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') } diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb new file mode 100644 index 00000000000..0ece3bff8f9 --- /dev/null +++ b/spec/requests/api/project_packages_spec.rb @@ -0,0 +1,272 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::ProjectPackages do + let(:user) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let!(:package1) { create(:npm_package, project: project, version: '3.1.0', name: "@#{project.root_namespace.path}/foo1") } + let(:package_url) { "/projects/#{project.id}/packages/#{package1.id}" } + let!(:package2) { create(:nuget_package, project: project, version: '2.0.4') } + let!(:another_package) { create(:npm_package) } + let(:no_package_url) { "/projects/#{project.id}/packages/0" } + let(:wrong_package_url) { "/projects/#{project.id}/packages/#{another_package.id}" } + + describe 'GET /projects/:id/packages' do + let(:url) { "/projects/#{project.id}/packages" } + let(:package_schema) { 'public_api/v4/packages/packages' } + + subject { get api(url) } + + context 'without the need for a license' do + context 'project is public' do + it_behaves_like 'returns packages', :project, :no_type + end + + context 'project is private' do + let(:project) { create(:project, :private) } + + context 'for unauthenticated user' do + it_behaves_like 'rejects packages access', :project, :no_type, :not_found + end + + context 'for authenticated user' do + subject { get api(url, user) } + + it_behaves_like 'returns packages', :project, :maintainer + it_behaves_like 'returns packages', :project, :developer + it_behaves_like 'returns packages', :project, :reporter + it_behaves_like 'rejects packages access', :project, :no_type, :not_found + it_behaves_like 'rejects packages access', :project, :guest, :forbidden + + context 'user is a maintainer' do + before do + project.add_maintainer(user) + end + + it 'returns the destroy url' do + subject + + expect(json_response.first['_links']).to include('delete_api_path') + end + end + end + end + + context 'with pagination params' do + let!(:package3) { create(:maven_package, project: project) } + let!(:package4) { create(:maven_package, project: project) } + + context 'with pagination params' do + let!(:package3) { create(:npm_package, project: project) } + let!(:package4) { create(:npm_package, project: project) } + + it_behaves_like 'returns paginated packages' + end + end + + context 'with sorting' do + let(:package3) { create(:maven_package, project: project, version: '1.1.1', name: 'zzz') } + + before do + travel_to(1.day.ago) do + package3 + end + end + + it_behaves_like 'package sorting', 'name' do + let(:packages) { [package1, package2, package3] } + end + + it_behaves_like 'package sorting', 'created_at' do + let(:packages) { [package3, package1, package2] } + end + + it_behaves_like 'package sorting', 'version' do + let(:packages) { [package3, package2, package1] } + end + + it_behaves_like 'package sorting', 'type' do + let(:packages) { [package3, package1, package2] } + end + end + + it_behaves_like 'filters on each package_type', is_project: true + + context 'filtering on package_name' do + include_context 'package filter context' + + it 'returns the named package' do + url = package_filter_url(:name, 'nuget') + get api(url, user) + + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to include(package2.name) + end + end + end + end + + describe 'GET /projects/:id/packages/:package_id' do + subject { get api(package_url, user) } + + shared_examples 'no destroy url' do + it 'returns no destroy url' do + subject + + expect(json_response['_links']).not_to include('delete_api_path') + end + end + + shared_examples 'destroy url' do + it 'returns destroy url' do + subject + + expect(json_response['_links']['delete_api_path']).to be_present + end + end + + context 'without the need for a license' do + context 'project is public' do + it 'returns 200 and the package information' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/packages/package') + end + + it 'returns 404 when the package does not exist' do + get api(no_package_url, user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 404 for the package from a different project' do + get api(wrong_package_url, user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it_behaves_like 'no destroy url' + end + + context 'project is private' do + let(:project) { create(:project, :private) } + + it 'returns 404 for non authenticated user' do + get api(package_url) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 404 for a user without access to the project' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + + context 'user is a developer' do + before do + project.add_developer(user) + end + + it 'returns 200 and the package information' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/packages/package') + end + + it_behaves_like 'no destroy url' + end + + context 'user is a maintainer' do + before do + project.add_maintainer(user) + end + + it_behaves_like 'destroy url' + end + + context 'with pipeline' do + let!(:package1) { create(:npm_package, :with_build, project: project) } + + it 'returns the pipeline info' do + project.add_developer(user) + + get api(package_url, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/packages/package_with_build') + end + end + end + end + end + + describe 'DELETE /projects/:id/packages/:package_id' do + context 'without the need for a license' do + context 'project is public' do + it 'returns 403 for non authenticated user' do + delete api(package_url) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'returns 403 for a user without access to the project' do + delete api(package_url, user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'project is private' do + let(:project) { create(:project, :private) } + + it 'returns 404 for non authenticated user' do + delete api(package_url) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 404 for a user without access to the project' do + delete api(package_url, user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 404 when the package does not exist' do + project.add_maintainer(user) + + delete api(no_package_url, user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 404 for the package from a different project' do + project.add_maintainer(user) + + delete api(wrong_package_url, user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it 'returns 403 for a user without enough permissions' do + project.add_developer(user) + + delete api(package_url, user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'returns 204' do + project.add_maintainer(user) + + delete api(package_url, user) + + expect(response).to have_gitlab_http_status(:no_content) + end + end + end + end +end diff --git a/spec/requests/api/project_repository_storage_moves_spec.rb b/spec/requests/api/project_repository_storage_moves_spec.rb index 40966e31d0d..4c9e058ef13 100644 --- a/spec/requests/api/project_repository_storage_moves_spec.rb +++ b/spec/requests/api/project_repository_storage_moves_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::ProjectRepositoryStorageMoves do +RSpec.describe API::ProjectRepositoryStorageMoves do include AccessMatchersForRequest let_it_be(:user) { create(:admin) } diff --git a/spec/requests/api/project_snapshots_spec.rb b/spec/requests/api/project_snapshots_spec.rb index a54f317782b..f23e374407b 100644 --- a/spec/requests/api/project_snapshots_spec.rb +++ b/spec/requests/api/project_snapshots_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::ProjectSnapshots do +RSpec.describe API::ProjectSnapshots do include WorkhorseHelpers let(:project) { create(:project) } diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 22189dc3299..fbb0e3e109f 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -2,7 +2,9 @@ require 'spec_helper' -describe API::ProjectSnippets do +RSpec.describe API::ProjectSnippets do + include SnippetHelpers + let_it_be(:project) { create(:project, :public) } let_it_be(:user) { create(:user) } let_it_be(:admin) { create(:admin) } @@ -84,19 +86,22 @@ describe API::ProjectSnippets do end describe 'GET /projects/:project_id/snippets/:id' do - let(:user) { create(:user) } - let(:snippet) { create(:project_snippet, :public, :repository, project: project) } + let_it_be(:user) { create(:user) } + let_it_be(:snippet) { create(:project_snippet, :public, :repository, project: project) } it 'returns snippet json' do get api("/projects/#{project.id}/snippets/#{snippet.id}", user) - expect(response).to have_gitlab_http_status(:ok) + aggregate_failures do + 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['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) + 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 end it 'returns 404 for invalid snippet id' do @@ -111,6 +116,10 @@ describe API::ProjectSnippets do let(:request) { get api("/projects/#{project_no_snippets.id}/snippets/123", user) } end end + + it_behaves_like 'snippet_multiple_files feature disabled' do + subject { get api("/projects/#{project.id}/snippets/#{snippet.id}", user) } + end end describe 'POST /projects/:project_id/snippets/' do @@ -443,7 +452,7 @@ describe API::ProjectSnippets do get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin) expect(response).to have_gitlab_http_status(:ok) - expect(response.content_type).to eq 'text/plain' + expect(response.media_type).to eq 'text/plain' end it 'returns 404 for invalid snippet id' do @@ -465,4 +474,12 @@ describe API::ProjectSnippets do subject { get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", snippet.author) } 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) } + + it_behaves_like 'raw snippet files' do + let(:api_path) { "/projects/#{snippet.project.id}/snippets/#{snippet_id}/files/#{ref}/#{file_path}/raw" } + end + end end diff --git a/spec/requests/api/project_statistics_spec.rb b/spec/requests/api/project_statistics_spec.rb index 89809a97b96..5f0cac403aa 100644 --- a/spec/requests/api/project_statistics_spec.rb +++ b/spec/requests/api/project_statistics_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::ProjectStatistics do +RSpec.describe API::ProjectStatistics do let_it_be(:developer) { create(:user) } let_it_be(:public_project) { create(:project, :public) } diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb index caeb465080e..59b2b09f0bf 100644 --- a/spec/requests/api/project_templates_spec.rb +++ b/spec/requests/api/project_templates_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::ProjectTemplates do +RSpec.describe API::ProjectTemplates do let_it_be(:public_project) { create(:project, :public, path: 'path.with.dot') } let_it_be(:private_project) { create(:project, :private) } let_it_be(:developer) { create(:user) } diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index c3f29ec47a9..76b0c04e32d 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -shared_examples 'languages and percentages JSON response' do +RSpec.shared_examples 'languages and percentages JSON response' do let(:expected_languages) { project.repository.languages.map { |language| language.values_at(:label, :value)}.to_h } before do @@ -46,7 +46,7 @@ shared_examples 'languages and percentages JSON response' do end end -describe API::Projects do +RSpec.describe API::Projects do include ProjectForksHelper let(:user) { create(:user) } @@ -254,7 +254,10 @@ describe API::Projects 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.first).to include 'statistics' + + statistics = json_response.first['statistics'] + expect(statistics).to be_present + expect(statistics).to include('commit_count', 'storage_size', 'repository_size', 'wiki_size', 'lfs_objects_size', 'job_artifacts_size', 'snippets_size') end it "does not include license by default" do @@ -584,6 +587,85 @@ describe API::Projects do end end + context 'sorting by project statistics' do + %w(repository_size storage_size wiki_size).each do |order_by| + context "sorting by #{order_by}" do + before do + ProjectStatistics.update_all(order_by => 100) + project4.statistics.update_columns(order_by => 10) + project.statistics.update_columns(order_by => 200) + end + + context 'admin user' do + let(:current_user) { admin } + + 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 } + + 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['id']).to eq(project4.id) + end + end + + 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 } + + 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['id']).to eq(project.id) + end + end + end + + context 'non-admin user' do + let(:current_user) { user } + let(:projects) { [public_project, project, project2, project3] } + + it 'returns projects ordered normally' do + get api('/projects', current_user), params: { order_by: order_by } + + 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 { |project| project['id'] }).to eq(projects.map(&:id).reverse) + end + end + end + end + end + + context 'filtering by repository_storage' do + before do + [project, project3].each { |proj| proj.update_columns(repository_storage: 'nfs-11') } + # Since we don't actually have Gitaly configured with an nfs-11 storage, an error would be raised + # when we present the projects in a response, as we ask Gitaly for stuff like default branch and Gitaly + # is not configured for a nfs-11 storage. So we trick Rails into thinking the storage for these projects + # is still default (in reality, it is). + allow_any_instance_of(Project).to receive(:repository_storage).and_return('default') + end + + context 'admin user' do + it_behaves_like 'projects response' do + let(:filter) { { repository_storage: 'nfs-11' } } + let(:current_user) { admin } + let(:projects) { [project, project3] } + end + end + + context 'non-admin user' do + it_behaves_like 'projects response' do + let(:filter) { { repository_storage: 'nfs-11' } } + let(:current_user) { user } + let(:projects) { [public_project, project, project2, project3] } + end + end + end + context 'with keyset pagination' do let(:current_user) { user } let(:projects) { [public_project, project, project2, project3] } @@ -1846,6 +1928,13 @@ describe API::Projects do end end end + + it 'exposes service desk attributes' do + get api("/projects/#{project.id}", user) + + expect(json_response).to have_key 'service_desk_enabled' + expect(json_response).to have_key 'service_desk_address' + end end describe 'GET /projects/:id/users' do @@ -2133,7 +2222,7 @@ describe API::Projects do expect(json_response['expires_at']).to eq(expires_at.to_s) end - it 'updates project authorization' 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 } end.to( @@ -2590,6 +2679,26 @@ describe API::Projects do end end end + + context 'when updating service desk' do + subject { put(api("/projects/#{project.id}", user), params: { service_desk_enabled: true }) } + + before do + project.update!(service_desk_enabled: false) + + allow(::Gitlab::IncomingEmail).to receive(:enabled?).and_return(true) + end + + it 'returns 200' do + subject + + 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) + end + end end describe 'POST /projects/:id/archive' do diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb index 9203e0ec819..8bcd493eb1f 100644 --- a/spec/requests/api/protected_branches_spec.rb +++ b/spec/requests/api/protected_branches_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::ProtectedBranches do +RSpec.describe API::ProtectedBranches do let(:user) { create(:user) } let!(:project) { create(:project, :repository) } let(:protected_name) { 'feature' } diff --git a/spec/requests/api/protected_tags_spec.rb b/spec/requests/api/protected_tags_spec.rb index 3bc8ecbee73..cc7261dafc9 100644 --- a/spec/requests/api/protected_tags_spec.rb +++ b/spec/requests/api/protected_tags_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::ProtectedTags do +RSpec.describe API::ProtectedTags do let(:user) { create(:user) } let!(:project) { create(:project, :repository) } let(:project2) { create(:project, path: 'project2', namespace: user.namespace) } diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb new file mode 100644 index 00000000000..b4e83c8caab --- /dev/null +++ b/spec/requests/api/pypi_packages_spec.rb @@ -0,0 +1,259 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe API::PypiPackages do + include WorkhorseHelpers + include PackagesManagerApiSpecHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project, reload: true) { create(:project, :public) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } + 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) } + + describe 'GET /api/v4/projects/:id/packages/pypi/simple/:package_name' do + let_it_be(:package) { create(:pypi_package, project: project) } + let(:url) { "/projects/#{project.id}/packages/pypi/simple/#{package.name}" } + + subject { get api(url) } + + context 'without the need for a license' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'PyPi package versions' | :success + 'PUBLIC' | :guest | true | true | 'PyPi package versions' | :success + 'PUBLIC' | :developer | true | false | 'PyPi package versions' | :success + 'PUBLIC' | :guest | true | false | 'PyPi package versions' | :success + 'PUBLIC' | :developer | false | true | 'PyPi package versions' | :success + 'PUBLIC' | :guest | false | true | 'PyPi package versions' | :success + 'PUBLIC' | :developer | false | false | 'PyPi package versions' | :success + 'PUBLIC' | :guest | false | false | 'PyPi package versions' | :success + 'PUBLIC' | :anonymous | false | true | 'PyPi package versions' | :success + 'PRIVATE' | :developer | true | true | 'PyPi package versions' | :success + 'PRIVATE' | :guest | true | true | 'process PyPi api request' | :forbidden + 'PRIVATE' | :developer | true | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :guest | true | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :developer | false | true | 'process PyPi api request' | :not_found + 'PRIVATE' | :guest | false | true | 'process PyPi api request' | :not_found + 'PRIVATE' | :developer | false | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :guest | false | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'process PyPi api request' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + it_behaves_like 'deploy token for package GET requests' + + it_behaves_like 'rejects PyPI access with unknown project id' + end + end + + describe 'POST /api/v4/projects/:id/packages/pypi/authorize' do + let_it_be(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } + let_it_be(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } } + let(:url) { "/projects/#{project.id}/packages/pypi/authorize" } + let(:headers) { {} } + + subject { post api(url), headers: headers } + + context 'without the need for a license' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process PyPi api request' | :success + 'PUBLIC' | :guest | true | true | 'process PyPi api request' | :forbidden + 'PUBLIC' | :developer | true | false | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :guest | true | false | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :developer | false | true | 'process PyPi api request' | :forbidden + 'PUBLIC' | :guest | false | true | 'process PyPi api request' | :forbidden + 'PUBLIC' | :developer | false | false | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :guest | false | false | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process PyPi api request' | :success + 'PRIVATE' | :guest | true | true | 'process PyPi api request' | :forbidden + 'PRIVATE' | :developer | true | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :guest | true | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :developer | false | true | 'process PyPi api request' | :not_found + 'PRIVATE' | :guest | false | true | 'process PyPi api request' | :not_found + 'PRIVATE' | :developer | false | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :guest | false | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'process PyPi api request' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:user_headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + let(:headers) { user_headers.merge(workhorse_header) } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + it_behaves_like 'deploy token for package uploads' + + it_behaves_like 'rejects PyPI access with unknown project id' + end + end + + describe 'POST /api/v4/projects/:id/packages/pypi' do + let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } + let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } } + let_it_be(:file_name) { 'package.whl' } + let(:url) { "/projects/#{project.id}/packages/pypi" } + let(:headers) { {} } + let(:base_params) { { requires_python: '>=3.7', version: '1.0.0', name: 'sample-project', sha256_digest: '123' } } + let(:params) { base_params.merge(content: temp_file(file_name)) } + let(:send_rewritten_field) { true } + + subject do + workhorse_finalize( + api(url), + method: :post, + file_key: :content, + params: params, + headers: headers, + send_rewritten_field: send_rewritten_field + ) + end + + context 'without the need for a license' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'PyPi package creation' | :created + 'PUBLIC' | :guest | true | true | 'process PyPi api request' | :forbidden + 'PUBLIC' | :developer | true | false | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :guest | true | false | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :developer | false | true | 'process PyPi api request' | :forbidden + 'PUBLIC' | :guest | false | true | 'process PyPi api request' | :forbidden + 'PUBLIC' | :developer | false | false | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :guest | false | false | 'process PyPi api request' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process PyPi api request' | :created + 'PRIVATE' | :guest | true | true | 'process PyPi api request' | :forbidden + 'PRIVATE' | :developer | true | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :guest | true | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :developer | false | true | 'process PyPi api request' | :not_found + 'PRIVATE' | :guest | false | true | 'process PyPi api request' | :not_found + 'PRIVATE' | :developer | false | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :guest | false | false | 'process PyPi api request' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'process PyPi api request' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:user_headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + let(:headers) { user_headers.merge(workhorse_header) } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + context 'with an invalid package' do + let(:token) { personal_access_token.token } + let(:user_headers) { build_basic_auth_header(user.username, token) } + let(:headers) { user_headers.merge(workhorse_header) } + + before do + params[:name] = '.$/@!^*' + project.add_developer(user) + end + + it_behaves_like 'returning response status', :bad_request + end + + it_behaves_like 'deploy token for package uploads' + + it_behaves_like 'rejects PyPI access with unknown project id' + end + end + + describe 'GET /api/v4/projects/:id/packages/pypi/files/:sha256/*file_identifier' 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(:url) { "/projects/#{project.id}/packages/pypi/files/#{package.package_files.first.file_sha256}/#{package_name}-1.0.0.tar.gz" } + + subject { get api(url) } + + context 'without the need for a license' do + context 'with valid project' do + using RSpec::Parameterized::TableSyntax + + where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'PyPi package download' | :success + 'PUBLIC' | :guest | true | true | 'PyPi package download' | :success + 'PUBLIC' | :developer | true | false | 'PyPi package download' | :success + 'PUBLIC' | :guest | true | false | 'PyPi package download' | :success + 'PUBLIC' | :developer | false | true | 'PyPi package download' | :success + 'PUBLIC' | :guest | false | true | 'PyPi package download' | :success + 'PUBLIC' | :developer | false | false | 'PyPi package download' | :success + 'PUBLIC' | :guest | false | false | 'PyPi package download' | :success + 'PUBLIC' | :anonymous | false | true | 'PyPi package download' | :success + 'PRIVATE' | :developer | true | true | 'PyPi package download' | :success + 'PRIVATE' | :guest | true | true | 'PyPi package download' | :success + 'PRIVATE' | :developer | true | false | 'PyPi package download' | :success + 'PRIVATE' | :guest | true | false | 'PyPi package download' | :success + 'PRIVATE' | :developer | false | true | 'PyPi package download' | :success + 'PRIVATE' | :guest | false | true | 'PyPi package download' | :success + 'PRIVATE' | :developer | false | false | 'PyPi package download' | :success + 'PRIVATE' | :guest | false | false | 'PyPi package download' | :success + 'PRIVATE' | :anonymous | false | true | 'PyPi package download' | :success + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + context 'with deploy token headers' do + let(:headers) { build_basic_auth_header(deploy_token.username, deploy_token.token) } + + context 'valid token' do + it_behaves_like 'returning response status', :success + end + + context 'invalid token' do + let(:headers) { build_basic_auth_header('foo', 'bar') } + + it_behaves_like 'returning response status', :success + end + end + + it_behaves_like 'rejects PyPI access with unknown project id' + end + end +end diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb index cf2043ecc74..82d0d64eba4 100644 --- a/spec/requests/api/release/links_spec.rb +++ b/spec/requests/api/release/links_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Release::Links do +RSpec.describe API::Release::Links do let(:project) { create(:project, :repository, :private) } let(:maintainer) { create(:user) } let(:reporter) { create(:user) } diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index f4cb7f25990..5e8353d74c3 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Releases do +RSpec.describe API::Releases do let(:project) { create(:project, :repository, :private) } let(:maintainer) { create(:user) } let(:reporter) { create(:user) } diff --git a/spec/requests/api/remote_mirrors_spec.rb b/spec/requests/api/remote_mirrors_spec.rb index 3029b8443b0..436efb708fd 100644 --- a/spec/requests/api/remote_mirrors_spec.rb +++ b/spec/requests/api/remote_mirrors_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::RemoteMirrors do +RSpec.describe API::RemoteMirrors do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :repository, :remote_mirror) } let_it_be(:developer) { create(:user) { |u| project.add_developer(u) } } diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 55243e83017..36707f32d04 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' require 'mime/types' -describe API::Repositories do +RSpec.describe API::Repositories do include RepoHelpers include WorkhorseHelpers @@ -227,7 +227,8 @@ describe API::Repositories do end describe "GET /projects/:id/repository/archive(.:format)?:sha" do - let(:route) { "/projects/#{project.id}/repository/archive" } + let(:project_id) { CGI.escape(project.full_path) } + let(:route) { "/projects/#{project_id}/repository/archive" } before do allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false) @@ -246,7 +247,7 @@ describe API::Repositories do end it 'returns the repository archive archive.zip' do - get api("/projects/#{project.id}/repository/archive.zip", user) + get api("/projects/#{project_id}/repository/archive.zip", user) expect(response).to have_gitlab_http_status(:ok) @@ -257,7 +258,7 @@ describe API::Repositories do end it 'returns the repository archive archive.tar.bz2' do - get api("/projects/#{project.id}/repository/archive.tar.bz2", user) + get api("/projects/#{project_id}/repository/archive.tar.bz2", user) expect(response).to have_gitlab_http_status(:ok) @@ -277,7 +278,7 @@ describe API::Repositories do it 'rate limits user when thresholds hit' do allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true) - get api("/projects/#{project.id}/repository/archive.tar.bz2", user) + get api("/projects/#{project_id}/repository/archive.tar.bz2", user) expect(response).to have_gitlab_http_status(:too_many_requests) end @@ -302,6 +303,13 @@ describe API::Repositories do end end + context 'when unauthenticated and project path has dots' do + it_behaves_like 'repository archive' do + let(:project) { create(:project, :public, :repository, path: 'path.with.dot') } + let(:current_user) { nil } + end + end + context 'when unauthenticated', 'and project is private' do it_behaves_like '404 response' do let(:request) { get api(route) } diff --git a/spec/requests/api/resource_label_events_spec.rb b/spec/requests/api/resource_label_events_spec.rb index 7619399458a..a4a70d89812 100644 --- a/spec/requests/api/resource_label_events_spec.rb +++ b/spec/requests/api/resource_label_events_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::ResourceLabelEvents do +RSpec.describe API::ResourceLabelEvents do let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project, :public, namespace: user.namespace) } let_it_be(:label) { create(:label, project: project) } diff --git a/spec/requests/api/resource_milestone_events_spec.rb b/spec/requests/api/resource_milestone_events_spec.rb index b2e92fde5ee..5c81c2180d7 100644 --- a/spec/requests/api/resource_milestone_events_spec.rb +++ b/spec/requests/api/resource_milestone_events_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::ResourceMilestoneEvents do +RSpec.describe API::ResourceMilestoneEvents do let!(:user) { create(:user) } let!(:project) { create(:project, :public, namespace: user.namespace) } let!(:milestone) { create(:milestone, project: project) } diff --git a/spec/requests/api/resource_state_events_spec.rb b/spec/requests/api/resource_state_events_spec.rb new file mode 100644 index 00000000000..46ca9874395 --- /dev/null +++ b/spec/requests/api/resource_state_events_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::ResourceStateEvents do + let_it_be(:user) { create(:user) } + let_it_be(:project, reload: true) { create(:project, :public, namespace: user.namespace) } + + before_all do + project.add_developer(user) + end + + shared_examples 'resource_state_events API' do |parent_type, eventable_type, id_name| + describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events" do + let!(:event) { create_event } + + it "returns an array of resource state events" do + url = "/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events" + get api(url, 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.first['id']).to eq(event.id) + expect(json_response.first['state']).to eq(event.state.to_s) + end + + it "returns a 404 error when eventable id not found" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{non_existing_record_id}/resource_state_events", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns 404 when not authorized" do + parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + private_user = create(:user) + + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events", private_user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events/:event_id" do + let!(:event) { create_event } + + it "returns a resource state event by id" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{event.id}", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['id']).to eq(event.id) + expect(json_response['state']).to eq(event.state.to_s) + end + + it "returns 404 when not authorized" do + parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + private_user = create(:user) + + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{event.id}", private_user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns a 404 error if resource state event not found" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{non_existing_record_id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + describe 'pagination' do + # https://gitlab.com/gitlab-org/gitlab/-/issues/220192 + it 'returns the second page' do + create_event + event2 = create_event + + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events?page=2&per_page=1", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq '2' + expect(json_response.count).to eq(1) + expect(json_response.first['id']).to eq(event2.id) + end + end + + def create_event(state: :opened) + create(:resource_state_event, eventable.class.name.underscore => eventable, state: state) + end + end + + context 'when eventable is an Issue' do + it_behaves_like 'resource_state_events API', 'projects', 'issues', 'iid' do + let(:parent) { project } + let(:eventable) { create(:issue, project: project, author: user) } + end + end + + context 'when eventable is a Merge Request' do + it_behaves_like 'resource_state_events API', 'projects', 'merge_requests', 'iid' do + let(:parent) { project } + let(:eventable) { create(:merge_request, source_project: project, target_project: project, author: user) } + end + end +end diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index a02d804ee9b..1a93be98a67 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Search do +RSpec.describe API::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) } diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index 53265574e6a..5528a0c094f 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe API::Services do +RSpec.describe API::Services do let_it_be(:user) { create(:user) } let_it_be(:user2) { create(:user) } diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index e6dd1fecb69..602aacb6ced 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Settings, 'Settings' do +RSpec.describe API::Settings, 'Settings' do let(:user) { create(:user) } let_it_be(:admin) { create(:admin) } @@ -62,14 +62,14 @@ describe API::Settings, 'Settings' do default_projects_limit: 3, default_project_creation: 2, password_authentication_enabled_for_web: false, - repository_storages: ['custom'], + repository_storages: 'custom', plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com', sourcegraph_enabled: true, sourcegraph_url: 'https://sourcegraph.com', sourcegraph_public_only: false, default_snippet_visibility: 'internal', - restricted_visibility_levels: ['public'], + restricted_visibility_levels: 'public', default_artifacts_expire_in: '2 days', help_page_text: 'custom help text', help_page_hide_commercial_content: true, @@ -94,7 +94,9 @@ describe API::Settings, 'Settings' do issues_create_limit: 300, raw_blob_request_limit: 300, spam_check_endpoint_enabled: true, - spam_check_endpoint_url: 'https://example.com/spam_check' + spam_check_endpoint_url: 'https://example.com/spam_check', + disabled_oauth_sign_in_sources: 'unknown', + import_sources: 'github,bitbucket' } expect(response).to have_gitlab_http_status(:ok) @@ -135,6 +137,8 @@ describe API::Settings, 'Settings' do expect(json_response['raw_blob_request_limit']).to eq(300) expect(json_response['spam_check_endpoint_enabled']).to be_truthy expect(json_response['spam_check_endpoint_url']).to eq('https://example.com/spam_check') + expect(json_response['disabled_oauth_sign_in_sources']).to eq([]) + expect(json_response['import_sources']).to match_array(%w(github bitbucket)) end end diff --git a/spec/requests/api/sidekiq_metrics_spec.rb b/spec/requests/api/sidekiq_metrics_spec.rb index 705ae29d5d8..23ac2ea5c0b 100644 --- a/spec/requests/api/sidekiq_metrics_spec.rb +++ b/spec/requests/api/sidekiq_metrics_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::SidekiqMetrics do +RSpec.describe API::SidekiqMetrics do let(:admin) { create(:user, :admin) } describe 'GET sidekiq/*' do diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index c12c95ae2e0..e676eb94337 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -2,14 +2,16 @@ require 'spec_helper' -describe API::Snippets do +RSpec.describe API::Snippets do + include SnippetHelpers + let_it_be(:user) { create(:user) } describe 'GET /snippets/' do it 'returns snippets available' do - public_snippet = create(:personal_snippet, :public, author: user) - private_snippet = create(:personal_snippet, :private, author: user) - internal_snippet = create(:personal_snippet, :internal, author: user) + public_snippet = create(:personal_snippet, :repository, :public, author: user) + private_snippet = create(:personal_snippet, :repository, :private, author: user) + internal_snippet = create(:personal_snippet, :repository, :internal, author: user) get api("/snippets/", user) @@ -22,6 +24,7 @@ describe API::Snippets do private_snippet.id) expect(json_response.last).to have_key('web_url') expect(json_response.last).to have_key('raw_url') + expect(json_response.last).to have_key('files') expect(json_response.last).to have_key('visibility') end @@ -59,32 +62,33 @@ describe API::Snippets do end describe 'GET /snippets/public' do - let!(:other_user) { create(:user) } - let!(:public_snippet) { create(:personal_snippet, :public, author: user) } - let!(:private_snippet) { create(:personal_snippet, :private, author: user) } - let!(:internal_snippet) { create(:personal_snippet, :internal, author: user) } - let!(:public_snippet_other) { create(:personal_snippet, :public, author: other_user) } - let!(:private_snippet_other) { create(:personal_snippet, :private, author: other_user) } - let!(:internal_snippet_other) { create(:personal_snippet, :internal, author: other_user) } - let!(:public_snippet_project) { create(:project_snippet, :public, author: user) } - let!(:private_snippet_project) { create(:project_snippet, :private, author: user) } - let!(:internal_snippet_project) { create(:project_snippet, :internal, author: user) } + let_it_be(:other_user) { create(:user) } + let_it_be(:public_snippet) { create(:personal_snippet, :repository, :public, author: user) } + let_it_be(:private_snippet) { create(:personal_snippet, :repository, :private, author: user) } + let_it_be(:internal_snippet) { create(:personal_snippet, :repository, :internal, author: user) } + let_it_be(:public_snippet_other) { create(:personal_snippet, :repository, :public, author: other_user) } + let_it_be(:private_snippet_other) { create(:personal_snippet, :repository, :private, author: other_user) } + let_it_be(:internal_snippet_other) { create(:personal_snippet, :repository, :internal, author: other_user) } + let_it_be(:public_snippet_project) { create(:project_snippet, :repository, :public, author: user) } + let_it_be(:private_snippet_project) { create(:project_snippet, :repository, :private, author: user) } + let_it_be(:internal_snippet_project) { create(:project_snippet, :repository, :internal, author: user) } it 'returns all snippets with public visibility from all users' do get api("/snippets/public", 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.map { |snippet| snippet['id']} ).to contain_exactly( - public_snippet.id, - public_snippet_other.id) - expect(json_response.map { |snippet| snippet['web_url']} ).to contain_exactly( - "http://localhost/snippets/#{public_snippet.id}", - "http://localhost/snippets/#{public_snippet_other.id}") - expect(json_response.map { |snippet| snippet['raw_url']} ).to contain_exactly( - "http://localhost/snippets/#{public_snippet.id}/raw", - "http://localhost/snippets/#{public_snippet_other.id}/raw") + 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 { |snippet| snippet['id']} ).to contain_exactly( + public_snippet.id, + public_snippet_other.id) + expect(json_response.map { |snippet| snippet['web_url']} ).to contain_exactly( + "http://localhost/snippets/#{public_snippet.id}", + "http://localhost/snippets/#{public_snippet_other.id}") + expect(json_response[0]['files'].first).to eq snippet_blob_file(public_snippet_other.blobs.first) + expect(json_response[1]['files'].first).to eq snippet_blob_file(public_snippet.blobs.first) + end end end @@ -102,13 +106,8 @@ describe API::Snippets do get api("/snippets/#{snippet.id}/raw", author) expect(response).to have_gitlab_http_status(:ok) - expect(response.content_type).to eq 'text/plain' - end - - it 'forces attachment content disposition' do - get api("/snippets/#{snippet.id}/raw", author) - - expect(headers['Content-Disposition']).to match(/^attachment/) + expect(response.media_type).to eq 'text/plain' + expect(headers['Content-Disposition']).to match(/^inline/) end it 'returns 404 for invalid snippet id' do @@ -141,56 +140,88 @@ describe API::Snippets do end end + describe 'GET /snippets/:id/files/:ref/:file_path/raw' do + let_it_be(:snippet) { create(:personal_snippet, :repository, :private) } + + it_behaves_like 'raw snippet files' do + let(:api_path) { "/snippets/#{snippet_id}/files/#{ref}/#{file_path}/raw" } + end + end + describe 'GET /snippets/:id' do let_it_be(:admin) { create(:user, :admin) } let_it_be(:author) { create(:user) } let_it_be(:private_snippet) { create(:personal_snippet, :repository, :private, author: author) } let_it_be(:internal_snippet) { create(:personal_snippet, :repository, :internal, author: author) } + let(:snippet) { private_snippet } - it 'requires authentication' do - get api("/snippets/#{private_snippet.id}", nil) + subject { get api("/snippets/#{snippet.id}", user) } - expect(response).to have_gitlab_http_status(:unauthorized) + it 'hides private snippets from an ordinary user' do + subject + + expect(response).to have_gitlab_http_status(:not_found) end - it 'returns snippet json' do - get api("/snippets/#{private_snippet.id}", author) + context 'without a user' do + let(:user) { nil } - expect(response).to have_gitlab_http_status(:ok) + it 'requires authentication' do + subject - expect(json_response['title']).to eq(private_snippet.title) - expect(json_response['description']).to eq(private_snippet.description) - expect(json_response['file_name']).to eq(private_snippet.file_name_on_repo) - expect(json_response['visibility']).to eq(private_snippet.visibility) - expect(json_response['ssh_url_to_repo']).to eq(private_snippet.ssh_url_to_repo) - expect(json_response['http_url_to_repo']).to eq(private_snippet.http_url_to_repo) + expect(response).to have_gitlab_http_status(:unauthorized) + end end - it 'shows private snippets to an admin' do - get api("/snippets/#{private_snippet.id}", admin) + context 'with the author' do + let(:user) { author } - expect(response).to have_gitlab_http_status(:ok) - end + it 'returns snippet json' do + subject - it 'hides private snippets from an ordinary user' do - get api("/snippets/#{private_snippet.id}", user) + expect(response).to have_gitlab_http_status(:ok) - expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['title']).to eq(private_snippet.title) + expect(json_response['description']).to eq(private_snippet.description) + expect(json_response['file_name']).to eq(private_snippet.file_name_on_repo) + expect(json_response['files']).to eq(private_snippet.blobs.map { |blob| snippet_blob_file(blob) }) + expect(json_response['visibility']).to eq(private_snippet.visibility) + expect(json_response['ssh_url_to_repo']).to eq(private_snippet.ssh_url_to_repo) + expect(json_response['http_url_to_repo']).to eq(private_snippet.http_url_to_repo) + end end - it 'shows internal snippets to an ordinary user' do - get api("/snippets/#{internal_snippet.id}", user) + context 'with an admin' do + let(:user) { admin } - expect(response).to have_gitlab_http_status(:ok) + it 'shows private snippets to an admin' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'returns 404 for invalid snippet id' do + private_snippet.destroy + + subject + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Snippet Not Found') + end end - it 'returns 404 for invalid snippet id' do - private_snippet.destroy + context 'with an internal snippet' do + let(:snippet) { internal_snippet } - get api("/snippets/#{private_snippet.id}", admin) + it 'shows internal snippets to an ordinary user' do + subject - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['message']).to eq('404 Snippet Not Found') + expect(response).to have_gitlab_http_status(:ok) + end + end + + it_behaves_like 'snippet_multiple_files feature disabled' do + let(:user) { author } end end @@ -221,6 +252,7 @@ describe API::Snippets do expect(json_response['title']).to eq(params[:title]) expect(json_response['description']).to eq(params[:description]) expect(json_response['file_name']).to eq(params[:file_name]) + expect(json_response['files']).to eq(snippet.blobs.map { |blob| snippet_blob_file(blob) }) expect(json_response['visibility']).to eq(params[:visibility]) end @@ -251,6 +283,10 @@ describe API::Snippets do it_behaves_like 'snippet creation' + it_behaves_like 'snippet_multiple_files feature disabled' do + let(:snippet) { Snippet.find(json_response["id"]) } + end + context 'with an external user' do let(:user) { create(:user, :external) } diff --git a/spec/requests/api/statistics_spec.rb b/spec/requests/api/statistics_spec.rb index 5aea5c225a0..eab97b6916e 100644 --- a/spec/requests/api/statistics_spec.rb +++ b/spec/requests/api/statistics_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Statistics, 'Statistics' do +RSpec.describe API::Statistics, 'Statistics' do include ProjectForksHelper tables_to_analyze = %w[ projects diff --git a/spec/requests/api/submodules_spec.rb b/spec/requests/api/submodules_spec.rb index 2604dc18005..6b141d6d036 100644 --- a/spec/requests/api/submodules_spec.rb +++ b/spec/requests/api/submodules_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Submodules do +RSpec.describe API::Submodules do let(:user) { create(:user) } let!(:project) { create(:project, :repository, namespace: user.namespace ) } let(:guest) { create(:user) { |u| project.add_guest(u) } } diff --git a/spec/requests/api/suggestions_spec.rb b/spec/requests/api/suggestions_spec.rb index ffb8c811622..34d3c54d700 100644 --- a/spec/requests/api/suggestions_spec.rb +++ b/spec/requests/api/suggestions_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Suggestions do +RSpec.describe API::Suggestions do let(:project) { create(:project, :repository) } let(:user) { create(:user) } diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index 609aa615d33..01b46053d52 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::SystemHooks do +RSpec.describe API::SystemHooks do include StubRequests let(:user) { create(:user) } diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index 694802ce1b8..b029c0f5793 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Tags do +RSpec.describe API::Tags do let(:user) { create(:user) } let(:guest) { create(:user).tap { |u| project.add_guest(u) } } let(:project) { create(:project, :repository, creator: user, path: 'my.project') } diff --git a/spec/requests/api/task_completion_status_spec.rb b/spec/requests/api/task_completion_status_spec.rb index 4dd1e27bd4b..97ce858ba12 100644 --- a/spec/requests/api/task_completion_status_spec.rb +++ b/spec/requests/api/task_completion_status_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'task completion status response' do +RSpec.describe 'task completion status response' do let_it_be(:user) { create(:user) } let_it_be(:project) do create(:project, :public, creator_id: user.id, namespace: user.namespace) diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb index fae338b4ca3..e1c5bfd82c4 100644 --- a/spec/requests/api/templates_spec.rb +++ b/spec/requests/api/templates_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Templates do +RSpec.describe API::Templates do context 'the Template Entity' do before do get api('/templates/gitignores/Ruby') diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb index ec9db5566e3..c6cba39314b 100644 --- a/spec/requests/api/terraform/state_spec.rb +++ b/spec/requests/api/terraform/state_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Terraform::State do +RSpec.describe API::Terraform::State do include HttpBasicAuthHelpers let_it_be(:project) { create(:project) } @@ -59,10 +59,11 @@ describe API::Terraform::State do context 'with developer permissions' do let(:current_user) { developer } - it 'returns forbidden if the user cannot access the state' do + it 'returns terraform state belonging to a project of given state name' do request - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to eq(state.file.read) end end end @@ -94,10 +95,11 @@ describe API::Terraform::State do context 'with developer permissions' do let(:job) { create(:ci_build, project: project, user: developer) } - it 'returns forbidden if the user cannot access the state' do + it 'returns terraform state belonging to a project of given state name' do request - expect(response).to have_gitlab_http_status(:forbidden) + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to eq(state.file.read) end end end @@ -235,9 +237,43 @@ describe API::Terraform::State do expect(response).to have_gitlab_http_status(:ok) end + + context 'state is already locked' do + before do + state.update!(lock_xid: 'locked', locked_by_user: current_user) + end + + it 'returns an error' do + request + + expect(response).to have_gitlab_http_status(:conflict) + end + end + + context 'user does not have permission to lock the state' do + let(:current_user) { developer } + + it 'returns an error' do + request + + expect(response).to have_gitlab_http_status(:forbidden) + end + end end describe 'DELETE /projects/:id/terraform/state/:name/lock' do + let(:params) do + { + ID: lock_id, + Version: '0.1', + Operation: 'OperationTypePlan', + Info: '', + Who: "#{current_user.username}", + Created: Time.now.utc.iso8601(6), + Path: '' + } + end + before do state.lock_xid = '123-456' state.save! @@ -246,7 +282,7 @@ describe API::Terraform::State do subject(:request) { delete api("#{state_path}/lock"), headers: auth_header, params: params } context 'with the correct lock id' do - let(:params) { { ID: '123-456' } } + let(:lock_id) { '123-456' } it 'removes the terraform state lock' do request @@ -266,7 +302,7 @@ describe API::Terraform::State do end context 'with an incorrect lock id' do - let(:params) { { ID: '456-789' } } + let(:lock_id) { '456-789' } it 'returns an error' do request @@ -276,7 +312,7 @@ describe API::Terraform::State do end context 'with a longer than 255 character lock id' do - let(:params) { { ID: '0' * 256 } } + let(:lock_id) { '0' * 256 } it 'returns an error' do request @@ -284,5 +320,16 @@ describe API::Terraform::State do expect(response).to have_gitlab_http_status(:bad_request) end end + + context 'user does not have permission to unlock the state' do + let(:lock_id) { '123-456' } + let(:current_user) { developer } + + it 'returns an error' do + request + + expect(response).to have_gitlab_http_status(:forbidden) + end + end end end diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index 0bdc71a30e9..dfd0e13d84c 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Todos do +RSpec.describe API::Todos do let_it_be(:group) { create(:group) } let_it_be(:project_1) { create(:project, :repository, group: group) } let_it_be(:project_2) { create(:project) } diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 19b01cb7913..c51358bf659 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Triggers do +RSpec.describe API::Triggers do let_it_be(:user) { create(:user) } let_it_be(:user2) { create(:user) } diff --git a/spec/requests/api/user_counts_spec.rb b/spec/requests/api/user_counts_spec.rb index 688dfe11115..94e25d647fc 100644 --- a/spec/requests/api/user_counts_spec.rb +++ b/spec/requests/api/user_counts_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::UserCounts do +RSpec.describe API::UserCounts do let(:user) { create(:user) } let(:project) { create(:project, :public) } diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index e780f67bcab..17f9112c1d5 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Users, :do_not_mock_admin_mode do +RSpec.describe API::Users, :do_not_mock_admin_mode do let_it_be(:admin) { create(:admin) } let_it_be(:user, reload: true) { create(:user, username: 'user.with.dot') } let_it_be(:key) { create(:key, user: user) } @@ -910,6 +910,14 @@ describe API::Users, :do_not_mock_admin_mode do expect(user.reload.bio).to eq('') end + it 'updates user with nil bio' do + put api("/users/#{user.id}", admin), params: { bio: nil } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['bio']).to eq('') + expect(user.reload.bio).to eq('') + end + it "updates user with new password and forces reset on next login" do put api("/users/#{user.id}", admin), params: { password: '12345678' } diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb index f209a1d2e6e..7bb73e9664b 100644 --- a/spec/requests/api/variables_spec.rb +++ b/spec/requests/api/variables_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Variables do +RSpec.describe API::Variables do let(:user) { create(:user) } let(:user2) { create(:user) } let!(:project) { create(:project, creator_id: user.id) } @@ -54,6 +54,59 @@ describe API::Variables do expect(response).to have_gitlab_http_status(:not_found) end + + context 'when there are two variables with the same key on different env' do + let!(:var1) { create(:ci_variable, project: project, key: 'key1', environment_scope: 'staging') } + let!(:var2) { create(:ci_variable, project: project, key: 'key1', environment_scope: 'production') } + + context 'when filter[environment_scope] is not passed' do + context 'FF ci_variables_api_filter_environment_scope is enabled' do + it 'returns 409' do + get api("/projects/#{project.id}/variables/key1", user) + + expect(response).to have_gitlab_http_status(:conflict) + end + end + + context 'FF ci_variables_api_filter_environment_scope is disabled' do + before do + stub_feature_flags(ci_variables_api_filter_environment_scope: false) + end + + it 'returns random one' do + get api("/projects/#{project.id}/variables/key1", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['key']).to eq('key1') + end + end + end + + context 'when filter[environment_scope] is passed' do + it 'returns the variable' do + get api("/projects/#{project.id}/variables/key1", user), params: { 'filter[environment_scope]': 'production' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['value']).to eq(var2.value) + end + end + + context 'when wrong filter[environment_scope] is passed' do + it 'returns not_found' do + get api("/projects/#{project.id}/variables/key1", user), params: { 'filter[environment_scope]': 'invalid' } + + expect(response).to have_gitlab_http_status(:not_found) + end + + context 'when there is only one variable with provided key' do + it 'returns not_found' do + get api("/projects/#{project.id}/variables/#{variable.key}", user), params: { 'filter[environment_scope]': 'invalid' } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + end end context 'authorized user with invalid permissions' do @@ -173,6 +226,52 @@ describe API::Variables do expect(response).to have_gitlab_http_status(:not_found) end + + context 'when there are two variables with the same key on different env' do + let!(:var1) { create(:ci_variable, project: project, key: 'key1', environment_scope: 'staging') } + let!(:var2) { create(:ci_variable, project: project, key: 'key1', environment_scope: 'production') } + + context 'when filter[environment_scope] is not passed' do + context 'FF ci_variables_api_filter_environment_scope is enabled' do + it 'returns 409' do + get api("/projects/#{project.id}/variables/key1", user) + + expect(response).to have_gitlab_http_status(:conflict) + end + end + + context 'FF ci_variables_api_filter_environment_scope is disabled' do + before do + stub_feature_flags(ci_variables_api_filter_environment_scope: false) + end + + it 'updates random one' do + put api("/projects/#{project.id}/variables/key1", user), params: { value: 'new_val' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['value']).to eq('new_val') + end + end + end + + context 'when filter[environment_scope] is passed' do + it 'updates the variable' do + put api("/projects/#{project.id}/variables/key1", user), params: { value: 'new_val', 'filter[environment_scope]': 'production' } + + expect(response).to have_gitlab_http_status(:ok) + expect(var1.reload.value).not_to eq('new_val') + expect(var2.reload.value).to eq('new_val') + end + end + + context 'when wrong filter[environment_scope] is passed' do + it 'returns not_found' do + put api("/projects/#{project.id}/variables/key1", user), params: { value: 'new_val', 'filter[environment_scope]': 'invalid' } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end end context 'authorized user with invalid permissions' do @@ -207,6 +306,56 @@ describe API::Variables do expect(response).to have_gitlab_http_status(:not_found) end + + context 'when there are two variables with the same key on different env' do + let!(:var1) { create(:ci_variable, project: project, key: 'key1', environment_scope: 'staging') } + let!(:var2) { create(:ci_variable, project: project, key: 'key1', environment_scope: 'production') } + + context 'when filter[environment_scope] is not passed' do + context 'FF ci_variables_api_filter_environment_scope is enabled' do + it 'returns 409' do + get api("/projects/#{project.id}/variables/key1", user) + + expect(response).to have_gitlab_http_status(:conflict) + end + end + + context 'FF ci_variables_api_filter_environment_scope is disabled' do + before do + stub_feature_flags(ci_variables_api_filter_environment_scope: false) + end + + it 'deletes random one' do + expect do + delete api("/projects/#{project.id}/variables/key1", user), params: { 'filter[environment_scope]': 'production' } + + expect(response).to have_gitlab_http_status(:no_content) + end.to change {project.variables.count}.by(-1) + end + end + end + + context 'when filter[environment_scope] is passed' do + it 'deletes the variable' do + expect do + delete api("/projects/#{project.id}/variables/key1", user), params: { 'filter[environment_scope]': 'production' } + + expect(response).to have_gitlab_http_status(:no_content) + end.to change {project.variables.count}.by(-1) + + expect(var1.reload).to be_present + expect { var2.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'when wrong filter[environment_scope] is passed' do + it 'returns not_found' do + delete api("/projects/#{project.id}/variables/key1", user), params: { 'filter[environment_scope]': 'invalid' } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end end context 'authorized user with invalid permissions' do diff --git a/spec/requests/api/version_spec.rb b/spec/requests/api/version_spec.rb index 9eb8c03e273..a0a0f66c8d1 100644 --- a/spec/requests/api/version_spec.rb +++ b/spec/requests/api/version_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe API::Version do +RSpec.describe API::Version do shared_examples_for 'GET /version' do context 'when unauthenticated' do it 'returns authentication error' do diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb index 43a5cb446bb..f271f8aa853 100644 --- a/spec/requests/api/wikis_spec.rb +++ b/spec/requests/api/wikis_spec.rb @@ -12,7 +12,7 @@ require 'spec_helper' # - maintainer # because they are 3 edge cases of using wiki pages. -describe API::Wikis do +RSpec.describe API::Wikis do include WorkhorseHelpers let(:user) { create(:user) } @@ -21,178 +21,10 @@ describe API::Wikis do let(:payload) { { content: 'content', format: 'rdoc', title: 'title' } } let(:expected_keys_with_content) { %w(content format slug title) } let(:expected_keys_without_content) { %w(format slug title) } + let(:wiki) { project_wiki } - shared_examples_for 'returns list of wiki pages' do - context 'when wiki has pages' do - let!(:pages) do - [create(:wiki_page, wiki: project_wiki, title: 'page1', content: 'content of page1'), - create(:wiki_page, wiki: project_wiki, title: 'page2.with.dot', content: 'content of page2')] - end - - it 'returns the list of wiki pages without content' do - get api(url, user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.size).to eq(2) - - json_response.each_with_index do |page, index| - expect(page.keys).to match_array(expected_keys_without_content) - expect(page['slug']).to eq(pages[index].slug) - expect(page['title']).to eq(pages[index].title) - end - end - - it 'returns the list of wiki pages with content' do - get api(url, user), params: { with_content: 1 } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.size).to eq(2) - - json_response.each_with_index do |page, index| - expect(page.keys).to match_array(expected_keys_with_content) - expect(page['content']).to eq(pages[index].content) - expect(page['slug']).to eq(pages[index].slug) - expect(page['title']).to eq(pages[index].title) - end - end - end - - it 'return the empty list of wiki pages' do - get api(url, user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.size).to eq(0) - end - end - - shared_examples_for 'returns wiki page' do - it 'returns the wiki page' do - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.size).to eq(4) - expect(json_response.keys).to match_array(expected_keys_with_content) - expect(json_response['content']).to eq(page.content) - expect(json_response['slug']).to eq(page.slug) - expect(json_response['title']).to eq(page.title) - end - end - - shared_examples_for 'creates wiki page' do - it 'creates the wiki page' do - post(api(url, user), params: payload) - - expect(response).to have_gitlab_http_status(:created) - expect(json_response.size).to eq(4) - expect(json_response.keys).to match_array(expected_keys_with_content) - expect(json_response['content']).to eq(payload[:content]) - expect(json_response['slug']).to eq(payload[:title].tr(' ', '-')) - expect(json_response['title']).to eq(payload[:title]) - expect(json_response['rdoc']).to eq(payload[:rdoc]) - end - - [:title, :content].each do |part| - it "responds with validation error on empty #{part}" do - payload.delete(part) - - post(api(url, user), params: payload) - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response.size).to eq(1) - expect(json_response['error']).to eq("#{part} is missing") - end - end - end - - shared_examples_for 'updates wiki page' do - it 'updates the wiki page' do - put(api(url, user), params: payload) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.size).to eq(4) - expect(json_response.keys).to match_array(expected_keys_with_content) - expect(json_response['content']).to eq(payload[:content]) - expect(json_response['slug']).to eq(payload[:title].tr(' ', '-')) - expect(json_response['title']).to eq(payload[:title]) - end - - [:title, :content, :format].each do |part| - it "updates with wiki with missing #{part}" do - payload.delete(part) - - put(api(url, user), params: payload) - - expect(response).to have_gitlab_http_status(:ok) - end - end - end - - shared_examples_for '403 Forbidden' do - it 'returns 403 Forbidden' do - expect(response).to have_gitlab_http_status(:forbidden) - expect(json_response.size).to eq(1) - expect(json_response['message']).to eq('403 Forbidden') - end - end - - shared_examples_for '404 Wiki Page Not Found' do - it 'returns 404 Wiki Page Not Found' do - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response.size).to eq(1) - expect(json_response['message']).to eq('404 Wiki Page Not Found') - end - end - - shared_examples_for '404 Project Not Found' do - it 'returns 404 Project Not Found' do - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response.size).to eq(1) - expect(json_response['message']).to eq('404 Project Not Found') - end - end - - shared_examples_for '204 No Content' do - it 'returns 204 No Content' do - expect(response).to have_gitlab_http_status(:no_content) - end - end - - shared_examples_for 'uploads wiki attachment' do - it 'pushes attachment to the wiki repository' do - allow(SecureRandom).to receive(:hex).and_return('fixed_hex') - - workhorse_post_with_file(api(url, user), file_key: :file, params: payload) - - expect(response).to have_gitlab_http_status(:created) - expect(json_response).to eq result_hash.deep_stringify_keys - end - - it 'responds with validation error on empty file' do - payload.delete(:file) - - post(api(url, user), params: payload) - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response.size).to eq(1) - expect(json_response['error']).to eq('file is missing') - end - - it 'responds with validation error on invalid temp file' do - payload[:file] = { tempfile: '/etc/hosts' } - - post(api(url, user), params: payload) - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response.size).to eq(1) - expect(json_response['error']).to eq('file is invalid') - end - - it 'is backward compatible with regular multipart uploads' do - allow(SecureRandom).to receive(:hex).and_return('fixed_hex') - - post(api(url, user), params: payload) - - expect(response).to have_gitlab_http_status(:created) - expect(json_response).to eq result_hash.deep_stringify_keys - end + shared_examples_for 'wiki API 404 Project Not Found' do + include_examples 'wiki API 404 Not Found', 'Project' end describe 'GET /projects/:id/wikis' do @@ -206,7 +38,7 @@ describe API::Wikis do get api(url) end - include_examples '404 Project Not Found' + include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do @@ -216,7 +48,7 @@ describe API::Wikis do get api(url, user) end - include_examples '403 Forbidden' + include_examples 'wiki API 403 Forbidden' end context 'when user is maintainer' do @@ -226,7 +58,7 @@ describe API::Wikis do get api(url, user) end - include_examples '403 Forbidden' + include_examples 'wiki API 403 Forbidden' end end @@ -238,7 +70,7 @@ describe API::Wikis do get api(url) end - include_examples '404 Project Not Found' + include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do @@ -246,7 +78,7 @@ describe API::Wikis do project.add_developer(user) end - include_examples 'returns list of wiki pages' + include_examples 'wikis API returns list of wiki pages' end context 'when user is maintainer' do @@ -254,7 +86,7 @@ describe API::Wikis do project.add_maintainer(user) end - include_examples 'returns list of wiki pages' + include_examples 'wikis API returns list of wiki pages' end end @@ -266,7 +98,7 @@ describe API::Wikis do get api(url) end - include_examples '404 Project Not Found' + include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do @@ -274,7 +106,7 @@ describe API::Wikis do project.add_developer(user) end - include_examples 'returns list of wiki pages' + include_examples 'wikis API returns list of wiki pages' end context 'when user is maintainer' do @@ -282,7 +114,7 @@ describe API::Wikis do project.add_maintainer(user) end - include_examples 'returns list of wiki pages' + include_examples 'wikis API returns list of wiki pages' end end end @@ -299,7 +131,7 @@ describe API::Wikis do get api(url) end - include_examples '404 Project Not Found' + include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do @@ -309,7 +141,7 @@ describe API::Wikis do get api(url, user) end - include_examples '403 Forbidden' + include_examples 'wiki API 403 Forbidden' end context 'when user is maintainer' do @@ -319,7 +151,7 @@ describe API::Wikis do get api(url, user) end - include_examples '403 Forbidden' + include_examples 'wiki API 403 Forbidden' end end @@ -331,7 +163,7 @@ describe API::Wikis do get api(url) end - include_examples '404 Project Not Found' + include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do @@ -340,12 +172,12 @@ describe API::Wikis do get api(url, user) end - include_examples 'returns wiki page' + include_examples 'wikis API returns wiki page' context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } - include_examples '404 Wiki Page Not Found' + include_examples 'wiki API 404 Wiki Page Not Found' end end @@ -356,12 +188,12 @@ describe API::Wikis do get api(url, user) end - include_examples 'returns wiki page' + include_examples 'wikis API returns wiki page' context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } - include_examples '404 Wiki Page Not Found' + include_examples 'wiki API 404 Wiki Page Not Found' end end end @@ -374,7 +206,7 @@ describe API::Wikis do get api(url) end - include_examples '404 Project Not Found' + include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do @@ -384,12 +216,12 @@ describe API::Wikis do get api(url, user) end - include_examples 'returns wiki page' + include_examples 'wikis API returns wiki page' context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } - include_examples '404 Wiki Page Not Found' + include_examples 'wiki API 404 Wiki Page Not Found' end end @@ -400,12 +232,12 @@ describe API::Wikis do get api(url, user) end - include_examples 'returns wiki page' + include_examples 'wikis API returns wiki page' context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } - include_examples '404 Wiki Page Not Found' + include_examples 'wiki API 404 Wiki Page Not Found' end end end @@ -423,7 +255,7 @@ describe API::Wikis do post(api(url), params: payload) end - include_examples '404 Project Not Found' + include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do @@ -432,7 +264,7 @@ describe API::Wikis do post(api(url, user), params: payload) end - include_examples '403 Forbidden' + include_examples 'wiki API 403 Forbidden' end context 'when user is maintainer' do @@ -441,7 +273,7 @@ describe API::Wikis do post(api(url, user), params: payload) end - include_examples '403 Forbidden' + include_examples 'wiki API 403 Forbidden' end end @@ -453,7 +285,7 @@ describe API::Wikis do post(api(url), params: payload) end - include_examples '404 Project Not Found' + include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do @@ -461,7 +293,7 @@ describe API::Wikis do project.add_developer(user) end - include_examples 'creates wiki page' + include_examples 'wikis API creates wiki page' end context 'when user is maintainer' do @@ -469,7 +301,7 @@ describe API::Wikis do project.add_maintainer(user) end - include_examples 'creates wiki page' + include_examples 'wikis API creates wiki page' end end @@ -481,7 +313,7 @@ describe API::Wikis do post(api(url), params: payload) end - include_examples '404 Project Not Found' + include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do @@ -489,7 +321,7 @@ describe API::Wikis do project.add_developer(user) end - include_examples 'creates wiki page' + include_examples 'wikis API creates wiki page' end context 'when user is maintainer' do @@ -497,7 +329,7 @@ describe API::Wikis do project.add_maintainer(user) end - include_examples 'creates wiki page' + include_examples 'wikis API creates wiki page' end end end @@ -515,7 +347,7 @@ describe API::Wikis do put(api(url), params: payload) end - include_examples '404 Project Not Found' + include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do @@ -525,7 +357,7 @@ describe API::Wikis do put(api(url, user), params: payload) end - include_examples '403 Forbidden' + include_examples 'wiki API 403 Forbidden' end context 'when user is maintainer' do @@ -535,7 +367,7 @@ describe API::Wikis do put(api(url, user), params: payload) end - include_examples '403 Forbidden' + include_examples 'wiki API 403 Forbidden' end end @@ -547,7 +379,7 @@ describe API::Wikis do put(api(url), params: payload) end - include_examples '404 Project Not Found' + include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do @@ -555,7 +387,7 @@ describe API::Wikis do project.add_developer(user) end - include_examples 'updates wiki page' + include_examples 'wikis API updates wiki page' context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } @@ -564,7 +396,7 @@ describe API::Wikis do put(api(url, user), params: payload) end - include_examples '404 Wiki Page Not Found' + include_examples 'wiki API 404 Wiki Page Not Found' end end @@ -573,7 +405,7 @@ describe API::Wikis do project.add_maintainer(user) end - include_examples 'updates wiki page' + include_examples 'wikis API updates wiki page' context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } @@ -582,7 +414,7 @@ describe API::Wikis do put(api(url, user), params: payload) end - include_examples '404 Wiki Page Not Found' + include_examples 'wiki API 404 Wiki Page Not Found' end end end @@ -595,7 +427,7 @@ describe API::Wikis do put(api(url), params: payload) end - include_examples '404 Project Not Found' + include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do @@ -603,7 +435,7 @@ describe API::Wikis do project.add_developer(user) end - include_examples 'updates wiki page' + include_examples 'wikis API updates wiki page' context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } @@ -612,7 +444,7 @@ describe API::Wikis do put(api(url, user), params: payload) end - include_examples '404 Wiki Page Not Found' + include_examples 'wiki API 404 Wiki Page Not Found' end end @@ -621,7 +453,7 @@ describe API::Wikis do project.add_maintainer(user) end - include_examples 'updates wiki page' + include_examples 'wikis API updates wiki page' context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } @@ -630,7 +462,7 @@ describe API::Wikis do put(api(url, user), params: payload) end - include_examples '404 Wiki Page Not Found' + include_examples 'wiki API 404 Wiki Page Not Found' end end end @@ -638,7 +470,7 @@ describe API::Wikis do context 'when wiki belongs to a group project' do let(:project) { create(:project, :wiki_repo, namespace: group) } - include_examples 'updates wiki page' + include_examples 'wikis API updates wiki page' end end @@ -654,7 +486,7 @@ describe API::Wikis do delete(api(url)) end - include_examples '404 Project Not Found' + include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do @@ -664,7 +496,7 @@ describe API::Wikis do delete(api(url, user)) end - include_examples '403 Forbidden' + include_examples 'wiki API 403 Forbidden' end context 'when user is maintainer' do @@ -674,7 +506,7 @@ describe API::Wikis do delete(api(url, user)) end - include_examples '403 Forbidden' + include_examples 'wiki API 403 Forbidden' end end @@ -686,7 +518,7 @@ describe API::Wikis do delete(api(url)) end - include_examples '404 Project Not Found' + include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do @@ -696,7 +528,7 @@ describe API::Wikis do delete(api(url, user)) end - include_examples '403 Forbidden' + include_examples 'wiki API 403 Forbidden' end context 'when user is maintainer' do @@ -706,7 +538,7 @@ describe API::Wikis do delete(api(url, user)) end - include_examples '204 No Content' + include_examples 'wiki API 204 No Content' end end @@ -718,7 +550,7 @@ describe API::Wikis do delete(api(url)) end - include_examples '404 Project Not Found' + include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do @@ -728,7 +560,7 @@ describe API::Wikis do delete(api(url, user)) end - include_examples '403 Forbidden' + include_examples 'wiki API 403 Forbidden' end context 'when user is maintainer' do @@ -738,12 +570,12 @@ describe API::Wikis do delete(api(url, user)) end - include_examples '204 No Content' + include_examples 'wiki API 204 No Content' context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } - include_examples '404 Wiki Page Not Found' + include_examples 'wiki API 404 Wiki Page Not Found' end end end @@ -755,7 +587,7 @@ describe API::Wikis do delete(api(url, user)) end - include_examples '204 No Content' + include_examples 'wiki API 204 No Content' end end @@ -783,7 +615,7 @@ describe API::Wikis do post(api(url), params: payload) end - include_examples '404 Project Not Found' + include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do @@ -792,7 +624,7 @@ describe API::Wikis do post(api(url, user), params: payload) end - include_examples '403 Forbidden' + include_examples 'wiki API 403 Forbidden' end context 'when user is maintainer' do @@ -801,7 +633,7 @@ describe API::Wikis do post(api(url, user), params: payload) end - include_examples '403 Forbidden' + include_examples 'wiki API 403 Forbidden' end end @@ -813,7 +645,7 @@ describe API::Wikis do post(api(url), params: payload) end - include_examples '404 Project Not Found' + include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do @@ -821,7 +653,7 @@ describe API::Wikis do project.add_developer(user) end - include_examples 'uploads wiki attachment' + include_examples 'wiki API uploads wiki attachment' end context 'when user is maintainer' do @@ -829,7 +661,7 @@ describe API::Wikis do project.add_maintainer(user) end - include_examples 'uploads wiki attachment' + include_examples 'wiki API uploads wiki attachment' end end @@ -841,7 +673,7 @@ describe API::Wikis do post(api(url), params: payload) end - include_examples '404 Project Not Found' + include_examples 'wiki API 404 Project Not Found' end context 'when user is developer' do @@ -849,7 +681,7 @@ describe API::Wikis do project.add_developer(user) end - include_examples 'uploads wiki attachment' + include_examples 'wiki API uploads wiki attachment' end context 'when user is maintainer' do @@ -857,7 +689,7 @@ describe API::Wikis do project.add_maintainer(user) end - include_examples 'uploads wiki attachment' + include_examples 'wiki API uploads wiki attachment' end end end |