summaryrefslogtreecommitdiff
path: root/spec/requests/api
diff options
context:
space:
mode:
Diffstat (limited to 'spec/requests/api')
-rw-r--r--spec/requests/api/access_requests_spec.rb2
-rw-r--r--spec/requests/api/boards_spec.rb4
-rw-r--r--spec/requests/api/branches_spec.rb4
-rw-r--r--spec/requests/api/ci/pipelines_spec.rb616
-rw-r--r--spec/requests/api/ci/runner/jobs_artifacts_spec.rb40
-rw-r--r--spec/requests/api/ci/runner/jobs_put_spec.rb61
-rw-r--r--spec/requests/api/commits_spec.rb27
-rw-r--r--spec/requests/api/composer_packages_spec.rb107
-rw-r--r--spec/requests/api/conan_instance_packages_spec.rb152
-rw-r--r--spec/requests/api/conan_packages_spec.rb954
-rw-r--r--spec/requests/api/conan_project_packages_spec.rb152
-rw-r--r--spec/requests/api/generic_packages_spec.rb82
-rw-r--r--spec/requests/api/graphql/boards/board_list_issues_query_spec.rb8
-rw-r--r--spec/requests/api/graphql/current_user_todos_spec.rb81
-rw-r--r--spec/requests/api/graphql/group/group_members_spec.rb67
-rw-r--r--spec/requests/api/graphql/group_query_spec.rb11
-rw-r--r--spec/requests/api/graphql/instance_statistics_measurements_spec.rb21
-rw-r--r--spec/requests/api/graphql/issue/issue_spec.rb126
-rw-r--r--spec/requests/api/graphql/metrics/dashboard_query_spec.rb155
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/add_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/boards/destroy_spec.rb79
-rw-r--r--spec/requests/api/graphql/mutations/boards/lists/create_spec.rb54
-rw-r--r--spec/requests/api/graphql/mutations/boards/lists/update_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/branches/create_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb51
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb31
-rw-r--r--spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb45
-rw-r--r--spec/requests/api/graphql/mutations/commits/create_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/design_management/upload_spec.rb32
-rw-r--r--spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_locked_spec.rb7
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_severity_spec.rb57
-rw-r--r--spec/requests/api/graphql/mutations/issues/update_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/create_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/image_diff_note_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/notes/destroy_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/notes/update/image_diff_note_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/notes/update/note_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/snippets/create_spec.rb126
-rw-r--r--spec/requests/api/graphql/mutations/snippets/destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/snippets/update_spec.rb105
-rw-r--r--spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/todos/mark_done_spec.rb11
-rw-r--r--spec/requests/api/graphql/mutations/todos/restore_spec.rb11
-rw-r--r--spec/requests/api/graphql/namespace/projects_spec.rb39
-rw-r--r--spec/requests/api/graphql/project/issue/designs/notes_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb148
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb3
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb46
-rw-r--r--spec/requests/api/graphql/project/release_spec.rb60
-rw-r--r--spec/requests/api/graphql/project/releases_spec.rb19
-rw-r--r--spec/requests/api/graphql/project_query_spec.rb48
-rw-r--r--spec/requests/api/graphql/user/starred_projects_query_spec.rb73
-rw-r--r--spec/requests/api/group_packages_spec.rb2
-rw-r--r--spec/requests/api/helpers_spec.rb2
-rw-r--r--spec/requests/api/internal/base_spec.rb4
-rw-r--r--spec/requests/api/internal/kubernetes_spec.rb124
-rw-r--r--spec/requests/api/issue_links_spec.rb207
-rw-r--r--spec/requests/api/issues/get_group_issues_spec.rb97
-rw-r--r--spec/requests/api/issues/issues_spec.rb45
-rw-r--r--spec/requests/api/jobs_spec.rb317
-rw-r--r--spec/requests/api/maven_packages_spec.rb14
-rw-r--r--spec/requests/api/members_spec.rb8
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb4
-rw-r--r--spec/requests/api/merge_requests_spec.rb152
-rw-r--r--spec/requests/api/notes_spec.rb4
-rw-r--r--spec/requests/api/nuget_packages_spec.rb12
-rw-r--r--spec/requests/api/oauth_tokens_spec.rb6
-rw-r--r--spec/requests/api/pages/internal_access_spec.rb2
-rw-r--r--spec/requests/api/pages/pages_spec.rb4
-rw-r--r--spec/requests/api/pages/private_access_spec.rb2
-rw-r--r--spec/requests/api/pages/public_access_spec.rb2
-rw-r--r--spec/requests/api/project_milestones_spec.rb6
-rw-r--r--spec/requests/api/project_packages_spec.rb18
-rw-r--r--spec/requests/api/project_snippets_spec.rb54
-rw-r--r--spec/requests/api/projects_spec.rb120
-rw-r--r--spec/requests/api/pypi_packages_spec.rb55
-rw-r--r--spec/requests/api/search_spec.rb89
-rw-r--r--spec/requests/api/settings_spec.rb11
-rw-r--r--spec/requests/api/snippets_spec.rb65
-rw-r--r--spec/requests/api/terraform/state_spec.rb12
-rw-r--r--spec/requests/api/todos_spec.rb23
-rw-r--r--spec/requests/api/usage_data_spec.rb79
-rw-r--r--spec/requests/api/users_spec.rb73
-rw-r--r--spec/requests/api/v3/github_spec.rb516
-rw-r--r--spec/requests/api/variables_spec.rb64
91 files changed, 3950 insertions, 2014 deletions
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 223d740a004..2af6c438fc9 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -90,7 +90,7 @@ RSpec.describe API::AccessRequests do
context 'when authenticated as a stranger' do
context "when access request is disabled for the #{source_type}" do
before do
- source.update(request_access_enabled: false)
+ source.update!(request_access_enabled: false)
end
it 'returns 403' do
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index f0d3afd0af7..a63198c5407 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -41,7 +41,7 @@ RSpec.describe API::Boards do
it 'creates a new issue board list for group labels' do
group = create(:group)
group_label = create(:group_label, group: group)
- board_parent.update(group: group)
+ board_parent.update!(group: group)
post api(url, user), params: { label_id: group_label.id }
@@ -54,7 +54,7 @@ RSpec.describe API::Boards do
group = create(:group)
sub_group = create(:group, parent: group)
group_label = create(:group_label, group: group)
- board_parent.update(group: sub_group)
+ board_parent.update!(group: sub_group)
group.add_developer(user)
sub_group.add_developer(user)
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 4b9b82b3a5b..5298f93886d 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -181,7 +181,7 @@ RSpec.describe API::Branches do
context 'when unauthenticated', 'and project is public' do
before do
- project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
end
it_behaves_like 'repository branches'
@@ -289,7 +289,7 @@ RSpec.describe API::Branches do
context 'when unauthenticated', 'and project is public' do
before do
- project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
end
it_behaves_like 'repository branch'
diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb
index 111bc933ea4..577b43e6e42 100644
--- a/spec/requests/api/ci/pipelines_spec.rb
+++ b/spec/requests/api/ci/pipelines_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe API::Ci::Pipelines do
let_it_be(:user) { create(:user) }
let_it_be(:non_member) { create(:user) }
+ let_it_be(:project2) { create(:project, creator: user) }
# We need to reload as the shared example 'pipelines visibility table' is changing project
let_it_be(:project, reload: true) do
@@ -307,6 +308,606 @@ RSpec.describe API::Ci::Pipelines do
end
end
+ describe 'GET /projects/:id/pipelines/:pipeline_id/jobs' do
+ let(:query) { {} }
+ let(:api_user) { user }
+ let_it_be(:job) do
+ create(:ci_build, :success, pipeline: pipeline,
+ artifacts_expire_at: 1.day.since)
+ end
+
+ let(:guest) { create(:project_member, :guest, project: project).user }
+
+ before do |example|
+ unless example.metadata[:skip_before_request]
+ project.update!(public_builds: false)
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), params: query
+ end
+ end
+
+ context 'with ci_jobs_finder_refactor ff enabled' do
+ before do
+ stub_feature_flags(ci_jobs_finder_refactor: true)
+ end
+
+ context 'authorized user' do
+ it 'returns pipeline jobs' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ end
+
+ it 'returns correct values' do
+ expect(json_response).not_to be_empty
+ expect(json_response.first['commit']['id']).to eq project.commit.id
+ expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
+ expect(json_response.first['artifacts_file']).to be_nil
+ expect(json_response.first['artifacts']).to be_an Array
+ expect(json_response.first['artifacts']).to be_empty
+ end
+
+ it_behaves_like 'a job with artifacts and trace' do
+ let(:api_endpoint) { "/projects/#{project.id}/pipelines/#{pipeline.id}/jobs" }
+ end
+
+ it 'returns pipeline data' do
+ json_job = json_response.first
+
+ expect(json_job['pipeline']).not_to be_empty
+ expect(json_job['pipeline']['id']).to eq job.pipeline.id
+ expect(json_job['pipeline']['ref']).to eq job.pipeline.ref
+ expect(json_job['pipeline']['sha']).to eq job.pipeline.sha
+ expect(json_job['pipeline']['status']).to eq job.pipeline.status
+ end
+
+ context 'filter jobs with one scope element' do
+ let(:query) { { 'scope' => 'pending' } }
+
+ it do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'filter jobs with hash' do
+ let(:query) { { scope: { hello: 'pending', world: 'running' } } }
+
+ it { expect(response).to have_gitlab_http_status(:bad_request) }
+ end
+
+ context 'filter jobs with array of scope elements' do
+ let(:query) { { scope: %w(pending running) } }
+
+ it do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'respond 400 when scope contains invalid state' do
+ let(:query) { { scope: %w(unknown running) } }
+
+ it { expect(response).to have_gitlab_http_status(:bad_request) }
+ end
+
+ context 'jobs in different pipelines' do
+ let!(:pipeline2) { create(:ci_empty_pipeline, project: project) }
+ let!(:job2) { create(:ci_build, pipeline: pipeline2) }
+
+ it 'excludes jobs from other pipelines' do
+ json_response.each { |job| expect(job['pipeline']['id']).to eq(pipeline.id) }
+ end
+ end
+
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), params: query
+ end.count
+
+ create_list(:ci_build, 3, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline)
+
+ expect do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), params: query
+ end.not_to exceed_all_query_limit(control_count)
+ end
+ end
+
+ context 'no pipeline is found' do
+ it 'does not return jobs' do
+ get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/jobs", user)
+
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'unauthorized user' do
+ context 'when user is not logged in' do
+ let(:api_user) { nil }
+
+ it 'does not return jobs' do
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user is guest' do
+ let(:guest) { create(:project_member, :guest, project: project).user }
+ let(:api_user) { guest }
+
+ it 'does not return jobs' do
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+ end
+
+ context 'with ci_jobs_finder ff disabled' do
+ before do
+ stub_feature_flags(ci_jobs_finder_refactor: false)
+ end
+
+ context 'authorized user' do
+ it 'returns pipeline jobs' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ end
+
+ it 'returns correct values' do
+ expect(json_response).not_to be_empty
+ expect(json_response.first['commit']['id']).to eq project.commit.id
+ expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
+ expect(json_response.first['artifacts_file']).to be_nil
+ expect(json_response.first['artifacts']).to be_an Array
+ expect(json_response.first['artifacts']).to be_empty
+ end
+
+ it_behaves_like 'a job with artifacts and trace' do
+ let(:api_endpoint) { "/projects/#{project.id}/pipelines/#{pipeline.id}/jobs" }
+ end
+
+ it 'returns pipeline data' do
+ json_job = json_response.first
+
+ expect(json_job['pipeline']).not_to be_empty
+ expect(json_job['pipeline']['id']).to eq job.pipeline.id
+ expect(json_job['pipeline']['ref']).to eq job.pipeline.ref
+ expect(json_job['pipeline']['sha']).to eq job.pipeline.sha
+ expect(json_job['pipeline']['status']).to eq job.pipeline.status
+ end
+
+ context 'filter jobs with one scope element' do
+ let(:query) { { 'scope' => 'pending' } }
+
+ it do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'filter jobs with hash' do
+ let(:query) { { scope: { hello: 'pending', world: 'running' } } }
+
+ it { expect(response).to have_gitlab_http_status(:bad_request) }
+ end
+
+ context 'filter jobs with array of scope elements' do
+ let(:query) { { scope: %w(pending running) } }
+
+ it do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'respond 400 when scope contains invalid state' do
+ let(:query) { { scope: %w(unknown running) } }
+
+ it { expect(response).to have_gitlab_http_status(:bad_request) }
+ end
+
+ context 'jobs in different pipelines' do
+ let!(:pipeline2) { create(:ci_empty_pipeline, project: project) }
+ let!(:job2) { create(:ci_build, pipeline: pipeline2) }
+
+ it 'excludes jobs from other pipelines' do
+ json_response.each { |job| expect(job['pipeline']['id']).to eq(pipeline.id) }
+ end
+ end
+
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), params: query
+ end.count
+
+ create_list(:ci_build, 3, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline)
+
+ expect do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), params: query
+ end.not_to exceed_all_query_limit(control_count)
+ end
+ end
+
+ context 'no pipeline is found' do
+ it 'does not return jobs' do
+ get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/jobs", user)
+
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'unauthorized user' do
+ context 'when user is not logged in' do
+ let(:api_user) { nil }
+
+ it 'does not return jobs' do
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user is guest' do
+ let(:guest) { create(:project_member, :guest, project: project).user }
+ let(:api_user) { guest }
+
+ it 'does not return jobs' do
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/pipelines/:pipeline_id/bridges' do
+ let_it_be(:bridge) { create(:ci_bridge, pipeline: pipeline) }
+ let(:downstream_pipeline) { create(:ci_pipeline) }
+
+ let!(:pipeline_source) do
+ create(:ci_sources_pipeline,
+ source_pipeline: pipeline,
+ source_project: project,
+ source_job: bridge,
+ pipeline: downstream_pipeline,
+ project: downstream_pipeline.project)
+ end
+
+ let(:query) { {} }
+ let(:api_user) { user }
+
+ before do |example|
+ unless example.metadata[:skip_before_request]
+ project.update!(public_builds: false)
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
+ end
+ end
+
+ context 'with ci_jobs_finder_refactor ff enabled' do
+ before do
+ stub_feature_flags(ci_jobs_finder_refactor: true)
+ end
+
+ context 'authorized user' do
+ it 'returns pipeline bridges' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ end
+
+ it 'returns correct values' do
+ expect(json_response).not_to be_empty
+ expect(json_response.first['commit']['id']).to eq project.commit.id
+ expect(json_response.first['id']).to eq bridge.id
+ expect(json_response.first['name']).to eq bridge.name
+ expect(json_response.first['stage']).to eq bridge.stage
+ end
+
+ it 'returns pipeline data' do
+ json_bridge = json_response.first
+
+ expect(json_bridge['pipeline']).not_to be_empty
+ expect(json_bridge['pipeline']['id']).to eq bridge.pipeline.id
+ expect(json_bridge['pipeline']['ref']).to eq bridge.pipeline.ref
+ expect(json_bridge['pipeline']['sha']).to eq bridge.pipeline.sha
+ expect(json_bridge['pipeline']['status']).to eq bridge.pipeline.status
+ end
+
+ it 'returns downstream pipeline data' do
+ json_bridge = json_response.first
+
+ expect(json_bridge['downstream_pipeline']).not_to be_empty
+ expect(json_bridge['downstream_pipeline']['id']).to eq downstream_pipeline.id
+ expect(json_bridge['downstream_pipeline']['ref']).to eq downstream_pipeline.ref
+ expect(json_bridge['downstream_pipeline']['sha']).to eq downstream_pipeline.sha
+ expect(json_bridge['downstream_pipeline']['status']).to eq downstream_pipeline.status
+ end
+
+ context 'filter bridges' do
+ before_all do
+ create_bridge(pipeline, :pending)
+ create_bridge(pipeline, :running)
+ end
+
+ context 'with one scope element' do
+ let(:query) { { 'scope' => 'pending' } }
+
+ it :skip_before_request do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(json_response.count).to eq 1
+ expect(json_response.first["status"]).to eq "pending"
+ end
+ end
+
+ context 'with array of scope elements' do
+ let(:query) { { scope: %w(pending running) } }
+
+ it :skip_before_request do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(json_response.count).to eq 2
+ json_response.each { |r| expect(%w(pending running).include?(r['status'])).to be true }
+ end
+ end
+ end
+
+ context 'respond 400 when scope contains invalid state' do
+ context 'in an array' do
+ let(:query) { { scope: %w(unknown running) } }
+
+ it { expect(response).to have_gitlab_http_status(:bad_request) }
+ end
+
+ context 'in a hash' do
+ let(:query) { { scope: { unknown: true } } }
+
+ it { expect(response).to have_gitlab_http_status(:bad_request) }
+ end
+
+ context 'in a string' do
+ let(:query) { { scope: "unknown" } }
+
+ it { expect(response).to have_gitlab_http_status(:bad_request) }
+ end
+ end
+
+ context 'bridges in different pipelines' do
+ let!(:pipeline2) { create(:ci_empty_pipeline, project: project) }
+ let!(:bridge2) { create(:ci_bridge, pipeline: pipeline2) }
+
+ it 'excludes bridges from other pipelines' do
+ json_response.each { |bridge| expect(bridge['pipeline']['id']).to eq(pipeline.id) }
+ end
+ end
+
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
+ end.count
+
+ 3.times { create_bridge(pipeline) }
+
+ expect do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
+ end.not_to exceed_all_query_limit(control_count)
+ end
+ end
+
+ context 'no pipeline is found' do
+ it 'does not return bridges' do
+ get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/bridges", user)
+
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'unauthorized user' do
+ context 'when user is not logged in' do
+ let(:api_user) { nil }
+
+ it 'does not return bridges' do
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user is guest' do
+ let(:api_user) { guest }
+ let(:guest) { create(:project_member, :guest, project: project).user }
+
+ it 'does not return bridges' do
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when user has no read_build access for project' do
+ before do
+ project.add_guest(api_user)
+ end
+
+ it 'does not return bridges' do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user)
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+ end
+
+ context 'with ci_jobs_finder_refactor ff disabled' do
+ before do
+ stub_feature_flags(ci_jobs_finder_refactor: false)
+ end
+
+ context 'authorized user' do
+ it 'returns pipeline bridges' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ end
+
+ it 'returns correct values' do
+ expect(json_response).not_to be_empty
+ expect(json_response.first['commit']['id']).to eq project.commit.id
+ expect(json_response.first['id']).to eq bridge.id
+ expect(json_response.first['name']).to eq bridge.name
+ expect(json_response.first['stage']).to eq bridge.stage
+ end
+
+ it 'returns pipeline data' do
+ json_bridge = json_response.first
+
+ expect(json_bridge['pipeline']).not_to be_empty
+ expect(json_bridge['pipeline']['id']).to eq bridge.pipeline.id
+ expect(json_bridge['pipeline']['ref']).to eq bridge.pipeline.ref
+ expect(json_bridge['pipeline']['sha']).to eq bridge.pipeline.sha
+ expect(json_bridge['pipeline']['status']).to eq bridge.pipeline.status
+ end
+
+ it 'returns downstream pipeline data' do
+ json_bridge = json_response.first
+
+ expect(json_bridge['downstream_pipeline']).not_to be_empty
+ expect(json_bridge['downstream_pipeline']['id']).to eq downstream_pipeline.id
+ expect(json_bridge['downstream_pipeline']['ref']).to eq downstream_pipeline.ref
+ expect(json_bridge['downstream_pipeline']['sha']).to eq downstream_pipeline.sha
+ expect(json_bridge['downstream_pipeline']['status']).to eq downstream_pipeline.status
+ end
+
+ context 'filter bridges' do
+ before_all do
+ create_bridge(pipeline, :pending)
+ create_bridge(pipeline, :running)
+ end
+
+ context 'with one scope element' do
+ let(:query) { { 'scope' => 'pending' } }
+
+ it :skip_before_request do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(json_response.count).to eq 1
+ expect(json_response.first["status"]).to eq "pending"
+ end
+ end
+
+ context 'with array of scope elements' do
+ let(:query) { { scope: %w(pending running) } }
+
+ it :skip_before_request do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(json_response.count).to eq 2
+ json_response.each { |r| expect(%w(pending running).include?(r['status'])).to be true }
+ end
+ end
+ end
+
+ context 'respond 400 when scope contains invalid state' do
+ context 'in an array' do
+ let(:query) { { scope: %w(unknown running) } }
+
+ it { expect(response).to have_gitlab_http_status(:bad_request) }
+ end
+
+ context 'in a hash' do
+ let(:query) { { scope: { unknown: true } } }
+
+ it { expect(response).to have_gitlab_http_status(:bad_request) }
+ end
+
+ context 'in a string' do
+ let(:query) { { scope: "unknown" } }
+
+ it { expect(response).to have_gitlab_http_status(:bad_request) }
+ end
+ end
+
+ context 'bridges in different pipelines' do
+ let!(:pipeline2) { create(:ci_empty_pipeline, project: project) }
+ let!(:bridge2) { create(:ci_bridge, pipeline: pipeline2) }
+
+ it 'excludes bridges from other pipelines' do
+ json_response.each { |bridge| expect(bridge['pipeline']['id']).to eq(pipeline.id) }
+ end
+ end
+
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
+ end.count
+
+ 3.times { create_bridge(pipeline) }
+
+ expect do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
+ end.not_to exceed_all_query_limit(control_count)
+ end
+ end
+
+ context 'no pipeline is found' do
+ it 'does not return bridges' do
+ get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/bridges", user)
+
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'unauthorized user' do
+ context 'when user is not logged in' do
+ let(:api_user) { nil }
+
+ it 'does not return bridges' do
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user is guest' do
+ let(:api_user) { guest }
+ let(:guest) { create(:project_member, :guest, project: project).user }
+
+ it 'does not return bridges' do
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when user has no read_build access for project' do
+ before do
+ project.add_guest(api_user)
+ end
+
+ it 'does not return bridges' do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user)
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+ end
+
+ def create_bridge(pipeline, status = :created)
+ create(:ci_bridge, status: status, pipeline: pipeline).tap do |bridge|
+ downstream_pipeline = create(:ci_pipeline)
+ create(:ci_sources_pipeline,
+ source_pipeline: pipeline,
+ source_project: pipeline.project,
+ source_job: bridge,
+ pipeline: downstream_pipeline,
+ project: downstream_pipeline.project)
+ end
+ end
+ end
+
describe 'POST /projects/:id/pipeline ' do
def expect_variables(variables, expected_variables)
variables.each_with_index do |variable, index|
@@ -476,17 +1077,18 @@ RSpec.describe API::Ci::Pipelines do
end
end
- context 'when config source is not ci' do
- let(:non_ci_config_source) { ::Ci::PipelineEnums.non_ci_config_source_values.first }
- let(:pipeline_not_ci) do
- create(:ci_pipeline, config_source: non_ci_config_source, project: project)
+ context 'when pipeline is a dangling pipeline' do
+ let(:dangling_source) { Enums::Ci::Pipeline.dangling_sources.each_value.first }
+
+ let(:dangling_pipeline) do
+ create(:ci_pipeline, source: dangling_source, project: project)
end
it 'returns the specified pipeline' do
- get api("/projects/#{project.id}/pipelines/#{pipeline_not_ci.id}", user)
+ get api("/projects/#{project.id}/pipelines/#{dangling_pipeline.id}", user)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['sha']).to eq(pipeline_not_ci.sha)
+ expect(json_response['sha']).to eq(dangling_pipeline.sha)
end
end
end
@@ -624,7 +1226,7 @@ RSpec.describe API::Ci::Pipelines do
end
it 'does not log an audit event' do
- expect { delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner) }.not_to change { SecurityEvent.count }
+ expect { delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner) }.not_to change { AuditEvent.count }
end
context 'when the pipeline has jobs' do
diff --git a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
index e5c60bb539b..97110b63ff6 100644
--- a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
@@ -143,6 +143,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response['TempPath']).to eq(JobArtifactUploader.workhorse_local_upload_path)
expect(json_response['RemoteObject']).to be_nil
+ expect(json_response['MaximumSize']).not_to be_nil
end
end
@@ -167,6 +168,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
expect(json_response['RemoteObject']).to have_key('StoreURL')
expect(json_response['RemoteObject']).to have_key('DeleteURL')
expect(json_response['RemoteObject']).to have_key('MultipartUpload')
+ expect(json_response['MaximumSize']).not_to be_nil
end
end
@@ -188,6 +190,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
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
+ expect(json_response['MaximumSize']).not_to be_nil
end
it 'fails to post too large artifact' do
@@ -235,36 +238,41 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
expect(json_response['ProcessLsif']).to be_truthy
end
- it 'adds ProcessLsifReferences header' do
- authorize_artifacts_with_token_in_headers(artifact_type: :lsif)
+ it 'tracks code_intelligence usage ping' do
+ tracking_params = {
+ event_names: 'i_source_code_code_intelligence',
+ start_date: Date.yesterday,
+ end_date: Date.today
+ }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['ProcessLsifReferences']).to be_truthy
+ expect { authorize_artifacts_with_token_in_headers(artifact_type: :lsif) }
+ .to change { Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(tracking_params) }
+ .by(1)
end
context 'code_navigation feature flag is disabled' do
- it 'responds with a forbidden error' do
+ before do
stub_feature_flags(code_navigation: false)
+ end
+
+ it 'responds with a forbidden error' do
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)
+ it 'does not track code_intelligence usage ping' do
+ tracking_params = {
+ event_names: 'i_source_code_code_intelligence',
+ start_date: Date.yesterday,
+ end_date: Date.today
+ }
- 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
+ expect { authorize_artifacts_with_token_in_headers(artifact_type: :lsif) }
+ .not_to change { Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(tracking_params) }
end
end
end
diff --git a/spec/requests/api/ci/runner/jobs_put_spec.rb b/spec/requests/api/ci/runner/jobs_put_spec.rb
index 025747f2f0c..183a3b26e00 100644
--- a/spec/requests/api/ci/runner/jobs_put_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_put_spec.rb
@@ -105,6 +105,67 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
it { expect(job).to be_unmet_prerequisites }
end
+
+ context 'when unmigrated live trace chunks exist' do
+ context 'when accepting trace feature is enabled' do
+ before do
+ stub_feature_flags(ci_accept_trace: true)
+ end
+
+ context 'when checksum is present' do
+ context 'when live trace chunk is still live' do
+ it 'responds with 202' do
+ update_job(state: 'success', checksum: 'crc32:12345678')
+
+ expect(job.pending_state).to be_present
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+ end
+
+ context 'when runner retries request after receiving 202' do
+ it 'responds with 202 and then with 200', :sidekiq_inline do
+ perform_enqueued_jobs do
+ update_job(state: 'success', checksum: 'crc32:12345678')
+ end
+
+ expect(job.reload.pending_state).to be_present
+ expect(response).to have_gitlab_http_status(:accepted)
+
+ perform_enqueued_jobs do
+ update_job(state: 'success', checksum: 'crc32:12345678')
+ end
+
+ expect(job.reload.pending_state).not_to be_present
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when live trace chunk has been migrated' do
+ before do
+ job.trace_chunks.first.update!(data_store: :database)
+ end
+
+ it 'responds with 200' do
+ update_job(state: 'success', checksum: 'crc:12345678')
+
+ expect(job.reload).to be_success
+ expect(job.pending_state).not_to be_present
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
+ context 'when checksum is not present' do
+ it 'responds with 200' do
+ update_job(state: 'success')
+
+ expect(job.reload).to be_success
+ expect(job.pending_state).not_to be_present
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+ end
end
context 'when trace is given' do
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 21ff0a94db9..d34244771ad 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -367,10 +367,31 @@ RSpec.describe API::Commits do
end
end
- it 'does not increment the usage counters using access token authentication' do
- expect(::Gitlab::UsageDataCounters::WebIdeCounter).not_to receive(:increment_commits_count)
+ context 'when using access token authentication' do
+ it 'does not increment the usage counters' do
+ expect(::Gitlab::UsageDataCounters::WebIdeCounter).not_to receive(:increment_commits_count)
+ expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_web_ide_edit_action)
- post api(url, user), params: valid_c_params
+ post api(url, user), params: valid_c_params
+ end
+ end
+
+ context 'when using warden' do
+ it 'increments usage counters', :clean_gitlab_redis_shared_state do
+ session_id = Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d')
+ session_hash = { 'warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]] }
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
+ end
+
+ cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
+
+ expect(::Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_commits_count)
+ expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_web_ide_edit_action)
+
+ post api(url), params: valid_c_params
+ end
end
context 'a new file in project repo' do
diff --git a/spec/requests/api/composer_packages_spec.rb b/spec/requests/api/composer_packages_spec.rb
index f5b8ebb545b..f5279af0483 100644
--- a/spec/requests/api/composer_packages_spec.rb
+++ b/spec/requests/api/composer_packages_spec.rb
@@ -26,30 +26,61 @@ RSpec.describe API::ComposerPackages do
group.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
end
- where(:project_visibility_level, :user_role, :member, :user_token, :include_package) do
- 'PUBLIC' | :developer | true | true | :include_package
- 'PUBLIC' | :developer | true | false | :include_package
- 'PUBLIC' | :developer | false | false | :include_package
- 'PUBLIC' | :developer | false | true | :include_package
- 'PUBLIC' | :guest | true | true | :include_package
- 'PUBLIC' | :guest | true | false | :include_package
- 'PUBLIC' | :guest | false | true | :include_package
- 'PUBLIC' | :guest | false | false | :include_package
- 'PUBLIC' | :anonymous | false | true | :include_package
- 'PRIVATE' | :developer | true | true | :include_package
- 'PRIVATE' | :developer | true | false | :does_not_include_package
- 'PRIVATE' | :developer | false | true | :does_not_include_package
- 'PRIVATE' | :developer | false | false | :does_not_include_package
- 'PRIVATE' | :guest | true | true | :does_not_include_package
- 'PRIVATE' | :guest | true | false | :does_not_include_package
- 'PRIVATE' | :guest | false | true | :does_not_include_package
- 'PRIVATE' | :guest | false | false | :does_not_include_package
- 'PRIVATE' | :anonymous | false | true | :does_not_include_package
+ context 'with basic auth' do
+ where(:project_visibility_level, :user_role, :member, :user_token, :include_package) do
+ 'PUBLIC' | :developer | true | true | :include_package
+ 'PUBLIC' | :developer | false | true | :include_package
+ 'PUBLIC' | :guest | true | true | :include_package
+ 'PUBLIC' | :guest | false | true | :include_package
+ 'PUBLIC' | :anonymous | false | true | :include_package
+ 'PRIVATE' | :developer | true | true | :include_package
+ 'PRIVATE' | :developer | false | true | :does_not_include_package
+ 'PRIVATE' | :guest | true | true | :does_not_include_package
+ 'PRIVATE' | :guest | false | true | :does_not_include_package
+ 'PRIVATE' | :anonymous | false | true | :does_not_include_package
+ 'PRIVATE' | :guest | false | false | :does_not_include_package
+ 'PRIVATE' | :guest | true | false | :does_not_include_package
+ 'PRIVATE' | :developer | false | false | :does_not_include_package
+ 'PRIVATE' | :developer | true | false | :does_not_include_package
+ 'PUBLIC' | :developer | true | false | :include_package
+ 'PUBLIC' | :guest | true | false | :include_package
+ 'PUBLIC' | :developer | false | false | :include_package
+ 'PUBLIC' | :guest | false | false | :include_package
+ end
+
+ with_them do
+ include_context 'Composer api project access', params[:project_visibility_level], params[:user_role], params[:user_token], :basic do
+ it_behaves_like 'Composer package index', params[:user_role], :success, params[:member], params[:include_package]
+ end
+ end
end
- with_them do
- include_context 'Composer api project access', params[:project_visibility_level], params[:user_role], params[:user_token] do
- it_behaves_like 'Composer package index', params[:user_role], :success, params[:member], params[:include_package]
+ context 'with private token header auth' do
+ where(:project_visibility_level, :user_role, :member, :user_token, :expected_status, :include_package) do
+ 'PUBLIC' | :developer | true | true | :success | :include_package
+ 'PUBLIC' | :developer | false | true | :success | :include_package
+ 'PUBLIC' | :guest | true | true | :success | :include_package
+ 'PUBLIC' | :guest | false | true | :success | :include_package
+ 'PUBLIC' | :anonymous | false | true | :success | :include_package
+ 'PRIVATE' | :developer | true | true | :success | :include_package
+ 'PRIVATE' | :developer | false | true | :success | :does_not_include_package
+ 'PRIVATE' | :guest | true | true | :success | :does_not_include_package
+ 'PRIVATE' | :guest | false | true | :success | :does_not_include_package
+ 'PRIVATE' | :anonymous | false | true | :success | :does_not_include_package
+ 'PRIVATE' | :guest | false | false | :unauthorized | nil
+ 'PRIVATE' | :guest | true | false | :unauthorized | nil
+ 'PRIVATE' | :developer | false | false | :unauthorized | nil
+ 'PRIVATE' | :developer | true | false | :unauthorized | nil
+ 'PUBLIC' | :developer | true | false | :unauthorized | nil
+ 'PUBLIC' | :guest | true | false | :unauthorized | nil
+ 'PUBLIC' | :developer | false | false | :unauthorized | nil
+ 'PUBLIC' | :guest | false | false | :unauthorized | nil
+ end
+
+ with_them do
+ include_context 'Composer api project access', params[:project_visibility_level], params[:user_role], params[:user_token], :token do
+ it_behaves_like 'Composer package index', params[:user_role], params[:expected_status], params[:member], params[:include_package]
+ end
end
end
end
@@ -105,22 +136,22 @@ RSpec.describe API::ComposerPackages do
context 'with valid project' do
where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'Composer provider index' | :success
- 'PUBLIC' | :developer | true | false | 'Composer provider index' | :success
+ 'PUBLIC' | :developer | true | false | 'process Composer api request' | :unauthorized
'PUBLIC' | :developer | false | true | 'Composer provider index' | :success
- 'PUBLIC' | :developer | false | false | 'Composer provider index' | :success
+ 'PUBLIC' | :developer | false | false | 'process Composer api request' | :unauthorized
'PUBLIC' | :guest | true | true | 'Composer provider index' | :success
- 'PUBLIC' | :guest | true | false | 'Composer provider index' | :success
+ 'PUBLIC' | :guest | true | false | 'process Composer api request' | :unauthorized
'PUBLIC' | :guest | false | true | 'Composer provider index' | :success
- 'PUBLIC' | :guest | false | false | 'Composer provider index' | :success
+ 'PUBLIC' | :guest | false | false | 'process Composer api request' | :unauthorized
'PUBLIC' | :anonymous | false | true | 'Composer provider index' | :success
'PRIVATE' | :developer | true | true | 'Composer provider index' | :success
- 'PRIVATE' | :developer | true | false | 'process Composer api request' | :not_found
+ 'PRIVATE' | :developer | true | false | 'process Composer api request' | :unauthorized
'PRIVATE' | :developer | false | true | 'process Composer api request' | :not_found
- 'PRIVATE' | :developer | false | false | 'process Composer api request' | :not_found
+ 'PRIVATE' | :developer | false | false | 'process Composer api request' | :unauthorized
'PRIVATE' | :guest | true | true | 'Composer empty provider index' | :success
- 'PRIVATE' | :guest | true | false | 'process Composer api request' | :not_found
+ 'PRIVATE' | :guest | true | false | 'process Composer api request' | :unauthorized
'PRIVATE' | :guest | false | true | 'process Composer api request' | :not_found
- 'PRIVATE' | :guest | false | false | 'process Composer api request' | :not_found
+ 'PRIVATE' | :guest | false | false | 'process Composer api request' | :unauthorized
'PRIVATE' | :anonymous | false | true | 'process Composer api request' | :not_found
end
@@ -151,22 +182,22 @@ RSpec.describe API::ComposerPackages do
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' | :developer | true | false | 'Composer package api request' | :success
+ 'PUBLIC' | :developer | true | false | 'process Composer api request' | :unauthorized
'PUBLIC' | :developer | false | true | 'Composer package api request' | :success
- 'PUBLIC' | :developer | false | false | 'Composer package api request' | :success
+ 'PUBLIC' | :developer | false | false | 'process Composer api request' | :unauthorized
'PUBLIC' | :guest | true | true | 'Composer package api request' | :success
- 'PUBLIC' | :guest | true | false | 'Composer package api request' | :success
+ 'PUBLIC' | :guest | true | false | 'process Composer api request' | :unauthorized
'PUBLIC' | :guest | false | true | 'Composer package api request' | :success
- 'PUBLIC' | :guest | false | false | 'Composer package api request' | :success
+ 'PUBLIC' | :guest | false | false | 'process Composer api request' | :unauthorized
'PUBLIC' | :anonymous | false | true | 'Composer package api request' | :success
'PRIVATE' | :developer | true | true | 'Composer package api request' | :success
- 'PRIVATE' | :developer | true | false | 'process Composer api request' | :not_found
+ 'PRIVATE' | :developer | true | false | 'process Composer api request' | :unauthorized
'PRIVATE' | :developer | false | true | 'process Composer api request' | :not_found
- 'PRIVATE' | :developer | false | false | 'process Composer api request' | :not_found
+ 'PRIVATE' | :developer | false | false | 'process Composer api request' | :unauthorized
'PRIVATE' | :guest | true | true | 'process Composer api request' | :not_found
- 'PRIVATE' | :guest | true | false | 'process Composer api request' | :not_found
+ 'PRIVATE' | :guest | true | false | 'process Composer api request' | :unauthorized
'PRIVATE' | :guest | false | true | 'process Composer api request' | :not_found
- 'PRIVATE' | :guest | false | false | 'process Composer api request' | :not_found
+ 'PRIVATE' | :guest | false | false | 'process Composer api request' | :unauthorized
'PRIVATE' | :anonymous | false | true | 'process Composer api request' | :not_found
end
diff --git a/spec/requests/api/conan_instance_packages_spec.rb b/spec/requests/api/conan_instance_packages_spec.rb
new file mode 100644
index 00000000000..817530f0bad
--- /dev/null
+++ b/spec/requests/api/conan_instance_packages_spec.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::ConanInstancePackages do
+ include_context 'conan api setup'
+
+ describe 'GET /api/v4/packages/conan/v1/ping' do
+ let_it_be(:url) { '/packages/conan/v1/ping' }
+
+ it_behaves_like 'conan ping endpoint'
+ end
+
+ describe 'GET /api/v4/packages/conan/v1/conans/search' do
+ let_it_be(:url) { '/packages/conan/v1/conans/search' }
+
+ it_behaves_like 'conan search endpoint'
+ end
+
+ describe 'GET /api/v4/packages/conan/v1/users/authenticate' do
+ let_it_be(:url) { '/packages/conan/v1/users/authenticate' }
+
+ it_behaves_like 'conan authenticate endpoint'
+ end
+
+ describe 'GET /api/v4/packages/conan/v1/users/check_credentials' do
+ let_it_be(:url) { "/packages/conan/v1/users/check_credentials" }
+
+ it_behaves_like 'conan check_credentials endpoint'
+ end
+
+ context 'recipe endpoints' do
+ include_context 'conan recipe endpoints'
+
+ let(:project_id) { 9999 }
+ let(:url_prefix) { "#{Settings.gitlab.base_url}/api/v4" }
+
+ describe 'GET /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do
+ let(:recipe_path) { package.conan_recipe_path }
+ let(:url) { "/packages/conan/v1/conans/#{recipe_path}" }
+
+ it_behaves_like 'recipe snapshot endpoint'
+ 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 }
+ let(:url) { "/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}" }
+
+ it_behaves_like 'package snapshot endpoint'
+ 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 'recipe download_urls endpoint'
+ 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 'package download_urls endpoint'
+ 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 'recipe download_urls endpoint'
+ 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 'package download_urls endpoint'
+ end
+
+ describe 'POST /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/upload_urls' do
+ subject { post api("/packages/conan/v1/conans/#{recipe_path}/upload_urls"), params: params.to_json, headers: headers }
+
+ it_behaves_like 'recipe upload_urls endpoint'
+ end
+
+ describe 'POST /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/upload_urls' do
+ subject { post api("/packages/conan/v1/conans/#{recipe_path}/packages/123456789/upload_urls"), params: params.to_json, headers: headers }
+
+ it_behaves_like 'package upload_urls endpoint'
+ end
+
+ describe 'DELETE /api/v4/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do
+ subject { delete api("/packages/conan/v1/conans/#{recipe_path}"), headers: headers}
+
+ it_behaves_like 'delete package endpoint'
+ end
+ end
+
+ context 'file download endpoints' do
+ include_context 'conan file download endpoints'
+
+ describe 'GET /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
+:recipe_revision/export/:file_name' do
+ subject do
+ get api("/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{recipe_file.file_name}"),
+ headers: headers
+ end
+
+ it_behaves_like 'recipe file download endpoint'
+ it_behaves_like 'project not found by recipe'
+ 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
+ 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 'package file download endpoint'
+ it_behaves_like 'project not found by recipe'
+ end
+ end
+
+ context 'file upload endpoints' do
+ include_context 'conan file upload endpoints'
+
+ describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name/authorize' do
+ let(:file_name) { 'conanfile.py' }
+
+ subject { put api("/packages/conan/v1/files/#{recipe_path}/0/export/#{file_name}/authorize"), headers: headers_with_token }
+
+ it_behaves_like 'workhorse authorize endpoint'
+ 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
+ let(:file_name) { 'conaninfo.txt' }
+
+ subject { put api("/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/#{file_name}/authorize"), headers: headers_with_token }
+
+ it_behaves_like 'workhorse authorize endpoint'
+ end
+
+ describe 'PUT /api/v4/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name' do
+ let(:url) { "/api/v4/packages/conan/v1/files/#{recipe_path}/0/export/#{file_name}" }
+
+ it_behaves_like 'workhorse recipe file upload endpoint'
+ 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(:url) { "/api/v4/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/#{file_name}" }
+
+ it_behaves_like 'workhorse package file upload endpoint'
+ end
+ end
+end
diff --git a/spec/requests/api/conan_packages_spec.rb b/spec/requests/api/conan_packages_spec.rb
deleted file mode 100644
index d24f6b68048..00000000000
--- a/spec/requests/api/conan_packages_spec.rb
+++ /dev/null
@@ -1,954 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe API::ConanPackages do
- include WorkhorseHelpers
- include HttpBasicAuthHelpers
- 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, status: :running) }
- 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
-
- it 'responds with 401 Unauthorized when the job is not running' do
- job.update!(status: :failed)
- 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(: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 invalid file_name' do |invalid_file_name|
- let(:file_name) { invalid_file_name }
-
- context 'with invalid file_name' do
- 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
-
- shared_examples 'rejects invalid upload_url params' do
- context 'with unaccepted json format' do
- let(:params) { %w[foo bar] }
-
- it 'returns 400' do
- subject
-
- expect(response).to have_gitlab_http_status(:bad_request)
- end
- end
- end
-
- shared_examples 'successful response when using Unicorn' do
- context 'on Unicorn', :unicorn do
- it 'returns successfully' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
- 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,
- 'conanmanifest.txt': 123 }
- end
-
- subject { post api("/packages/conan/v1/conans/#{recipe_path}/upload_urls"), params: params.to_json, headers: headers }
-
- it_behaves_like 'rejects invalid recipe'
- it_behaves_like 'rejects invalid upload_url params'
- it_behaves_like 'successful response when using Unicorn'
-
- 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
-
- context 'with conan_sources and conan_export files' do
- let(:params) do
- { 'conan_sources.tgz': 345,
- 'conan_export.tgz': 234,
- 'conanmanifest.txt': 123 }
- end
-
- it 'returns upload urls for the additional files' do
- subject
-
- expected_response = {
- 'conan_sources.tgz': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_sources.tgz",
- 'conan_export.tgz': "#{Settings.gitlab.base_url}/api/v4/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_export.tgz",
- '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
-
- context 'with an invalid file' do
- let(:params) do
- { 'invalid_file.txt': 10,
- 'conanmanifest.txt': 123 }
- end
-
- it 'does not return the invalid file as an upload_url' do
- subject
-
- expected_response = {
- '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
- 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,
- 'conanmanifest.txt': 123,
- 'conan_package.tgz': 523 }
- end
-
- subject { post api("/packages/conan/v1/conans/#{recipe_path}/packages/123456789/upload_urls"), params: params.to_json, headers: headers }
-
- it_behaves_like 'rejects invalid recipe'
- it_behaves_like 'rejects invalid upload_url params'
- it_behaves_like 'successful response when using Unicorn'
-
- 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
-
- context 'with invalid files' do
- let(:params) do
- { 'conaninfo.txt': 24,
- 'invalid_file.txt': 10 }
- end
-
- it 'returns upload urls only for the valid requested 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"
- }
-
- subject
-
- expect(response.body).to eq(expected_response.to_json)
- end
- 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 'rejects the request' do
- subject
-
- expect(response).to have_gitlab_http_status(:bad_request)
- end
- end
-
- context 'with a file' do
- it_behaves_like 'package workhorse uploads'
- 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
- let(:file_name) { 'conanfile.py' }
-
- subject { put api("/packages/conan/v1/files/#{recipe_path}/0/export/#{file_name}/authorize"), headers: headers_with_token }
-
- it_behaves_like 'rejects invalid recipe'
- it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack'
- 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
- let(:file_name) { 'conaninfo.txt' }
-
- subject { put api("/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/#{file_name}/authorize"), headers: headers_with_token }
-
- it_behaves_like 'rejects invalid recipe'
- it_behaves_like 'rejects invalid file_name', 'conaninfo.txttest'
- 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,
- send_rewritten_field: true,
- headers: headers_with_token
- )
- end
-
- it_behaves_like 'rejects invalid recipe'
- it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack'
- 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,
- send_rewritten_field: true
- )
- end
-
- it_behaves_like 'rejects invalid recipe'
- it_behaves_like 'rejects invalid file_name', 'conaninfo.txttest'
- 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/conan_project_packages_spec.rb b/spec/requests/api/conan_project_packages_spec.rb
new file mode 100644
index 00000000000..fefaf9790b1
--- /dev/null
+++ b/spec/requests/api/conan_project_packages_spec.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe API::ConanProjectPackages do
+ include_context 'conan api setup'
+
+ let(:project_id) { project.id }
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/ping' do
+ let(:url) { "/projects/#{project.id}/packages/conan/v1/ping" }
+
+ it_behaves_like 'conan ping endpoint'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/search' do
+ let(:url) { "/projects/#{project.id}/packages/conan/v1/conans/search" }
+
+ it_behaves_like 'conan search endpoint'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/users/authenticate' do
+ let(:url) { "/projects/#{project.id}/packages/conan/v1/users/authenticate" }
+
+ it_behaves_like 'conan authenticate endpoint'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/users/check_credentials' do
+ let(:url) { "/projects/#{project.id}/packages/conan/v1/users/check_credentials" }
+
+ it_behaves_like 'conan check_credentials endpoint'
+ end
+
+ context 'recipe endpoints' do
+ include_context 'conan recipe endpoints'
+
+ let(:url_prefix) { "#{Settings.gitlab.base_url}/api/v4/projects/#{project_id}" }
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do
+ let(:recipe_path) { package.conan_recipe_path }
+ let(:url) { "/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}" }
+
+ it_behaves_like 'recipe snapshot endpoint'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference' do
+ let(:recipe_path) { package.conan_recipe_path }
+ let(:url) { "/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}" }
+
+ it_behaves_like 'package snapshot endpoint'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/digest' do
+ subject { get api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/digest"), headers: headers }
+
+ it_behaves_like 'recipe download_urls endpoint'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/download_urls' do
+ subject { get api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/download_urls"), headers: headers }
+
+ it_behaves_like 'package download_urls endpoint'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/download_urls' do
+ subject { get api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/download_urls"), headers: headers }
+
+ it_behaves_like 'recipe download_urls endpoint'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/digest' do
+ subject { get api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/packages/#{conan_package_reference}/digest"), headers: headers }
+
+ it_behaves_like 'package download_urls endpoint'
+ end
+
+ describe 'POST /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/upload_urls' do
+ subject { post api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/upload_urls"), params: params.to_json, headers: headers }
+
+ it_behaves_like 'recipe upload_urls endpoint'
+ end
+
+ describe 'POST /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel/packages/:conan_package_reference/upload_urls' do
+ subject { post api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}/packages/123456789/upload_urls"), params: params.to_json, headers: headers }
+
+ it_behaves_like 'package upload_urls endpoint'
+ end
+
+ describe 'DELETE /api/v4/projects/:id/packages/conan/v1/conans/:package_name/package_version/:package_username/:package_channel' do
+ subject { delete api("/projects/#{project_id}/packages/conan/v1/conans/#{recipe_path}"), headers: headers}
+
+ it_behaves_like 'delete package endpoint'
+ end
+ end
+
+ context 'file download endpoints' do
+ include_context 'conan file download endpoints'
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
+:recipe_revision/export/:file_name' do
+ subject do
+ get api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/#{metadata.recipe_revision}/export/#{recipe_file.file_name}"),
+ headers: headers
+ end
+
+ it_behaves_like 'recipe file download endpoint'
+ it_behaves_like 'project not found by project id'
+ end
+
+ describe 'GET /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/
+:recipe_revision/package/:conan_package_reference/:package_revision/:file_name' do
+ subject do
+ get api("/projects/#{project_id}/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 'package file download endpoint'
+ it_behaves_like 'project not found by project id'
+ end
+ end
+
+ context 'file upload endpoints' do
+ include_context 'conan file upload endpoints'
+
+ describe 'PUT /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name/authorize' do
+ let(:file_name) { 'conanfile.py' }
+
+ subject { put api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/0/export/#{file_name}/authorize"), headers: headers_with_token }
+
+ it_behaves_like 'workhorse authorize endpoint'
+ end
+
+ describe 'PUT /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:conan_package_reference/:package_revision/:file_name/authorize' do
+ let(:file_name) { 'conaninfo.txt' }
+
+ subject { put api("/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/#{file_name}/authorize"), headers: headers_with_token }
+
+ it_behaves_like 'workhorse authorize endpoint'
+ end
+
+ describe 'PUT /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:file_name' do
+ let(:url) { "/api/v4/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/0/export/#{file_name}" }
+
+ it_behaves_like 'workhorse recipe file upload endpoint'
+ end
+
+ describe 'PUT /api/v4/projects/:id/packages/conan/v1/files/:package_name/package_version/:package_username/:package_channel/:recipe_revision/export/:conan_package_reference/:package_revision/:file_name' do
+ let(:url) { "/api/v4/projects/#{project_id}/packages/conan/v1/files/#{recipe_path}/0/package/123456789/0/#{file_name}" }
+
+ it_behaves_like 'workhorse package file upload endpoint'
+ end
+ end
+end
diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb
new file mode 100644
index 00000000000..ed852fe75c7
--- /dev/null
+++ b/spec/requests/api/generic_packages_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::GenericPackages do
+ let_it_be(:personal_access_token) { create(:personal_access_token) }
+ let_it_be(:project) { create(:project) }
+
+ describe 'GET /api/v4/projects/:id/packages/generic/ping' do
+ let(:user) { personal_access_token.user }
+ let(:auth_token) { personal_access_token.token }
+
+ before do
+ project.add_developer(user)
+ end
+
+ context 'packages feature is disabled' do
+ it 'responds with 404 Not Found' do
+ stub_packages_setting(enabled: false)
+
+ ping(personal_access_token: auth_token)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'generic_packages feature flag is disabled' do
+ it 'responds with 404 Not Found' do
+ stub_feature_flags(generic_packages: false)
+
+ ping(personal_access_token: auth_token)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'generic_packages feature flag is enabled' do
+ before do
+ stub_feature_flags(generic_packages: true)
+ end
+
+ context 'authenticating using personal access token' do
+ it 'responds with 200 OK when valid personal access token is provided' do
+ ping(personal_access_token: auth_token)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'responds with 401 Unauthorized when invalid personal access token provided' do
+ ping(personal_access_token: 'invalid-token')
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'authenticating using job token' do
+ it 'responds with 200 OK when valid job token is provided' do
+ job_token = create(:ci_build, :running, user: user).token
+
+ ping(job_token: job_token)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'responds with 401 Unauthorized when invalid job token provided' do
+ ping(job_token: 'invalid-token')
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+
+ def ping(personal_access_token: nil, job_token: nil)
+ headers = {
+ Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => personal_access_token.presence,
+ Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => job_token.presence
+ }.compact
+
+ get api('/projects/%d/packages/generic/ping' % project.id), headers: headers
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb
index ae1abb50a40..3628171fcc1 100644
--- a/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb
+++ b/spec/requests/api/graphql/boards/board_list_issues_query_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe 'get board lists' do
nodes {
lists {
nodes {
- issues {
+ issues(filters: {labelName: "#{label2.title}"}) {
count
nodes {
#{all_graphql_fields_for('issues'.classify)}
@@ -51,8 +51,8 @@ RSpec.describe 'get board lists' do
shared_examples 'group and project board list issues query' do
let!(:board) { create(:board, resource_parent: board_parent) }
let!(:label_list) { create(:list, board: board, label: label, position: 10) }
- let!(:issue1) { create(:issue, project: issue_project, labels: [label], relative_position: 9) }
- let!(:issue2) { create(:issue, project: issue_project, labels: [label], relative_position: 2) }
+ let!(:issue1) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 9) }
+ let!(:issue2) { create(:issue, project: issue_project, labels: [label, label2], relative_position: 2) }
let!(:issue3) { create(:issue, project: issue_project, labels: [label], relative_position: 9) }
let!(:issue4) { create(:issue, project: issue_project, labels: [label2], relative_position: 432) }
@@ -72,7 +72,7 @@ RSpec.describe 'get board lists' do
it 'can access the issues' do
post_graphql(query("id: \"#{global_id_of(label_list)}\""), current_user: user)
- expect(issue_titles).to eq([issue2.title, issue3.title, issue1.title])
+ expect(issue_titles).to eq([issue2.title, issue1.title])
end
end
end
diff --git a/spec/requests/api/graphql/current_user_todos_spec.rb b/spec/requests/api/graphql/current_user_todos_spec.rb
new file mode 100644
index 00000000000..b657f15d0e9
--- /dev/null
+++ b/spec/requests/api/graphql/current_user_todos_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'A Todoable that implements the CurrentUserTodos interface' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:todoable) { create(:issue, project: project) }
+ let_it_be(:done_todo) { create(:todo, state: :done, target: todoable, user: current_user) }
+ let_it_be(:pending_todo) { create(:todo, state: :pending, target: todoable, user: current_user) }
+ let(:state) { 'null' }
+
+ let(:todoable_response) do
+ graphql_data_at(:project, :issue, :currentUserTodos, :nodes)
+ end
+
+ let(:query) do
+ <<~GQL
+ {
+ project(fullPath: "#{project.full_path}") {
+ issue(iid: "#{todoable.iid}") {
+ currentUserTodos(state: #{state}) {
+ nodes {
+ #{all_graphql_fields_for('Todo', max_depth: 1)}
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'returns todos of the current user' do
+ post_graphql(query, current_user: current_user)
+
+ expect(todoable_response).to contain_exactly(
+ a_hash_including('id' => global_id_of(done_todo)),
+ a_hash_including('id' => global_id_of(pending_todo))
+ )
+ end
+
+ it 'does not return todos of another user', :aggregate_failures do
+ post_graphql(query, current_user: create(:user))
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(todoable_response).to be_empty
+ end
+
+ it 'does not error when there is no logged in user', :aggregate_failures do
+ post_graphql(query)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(todoable_response).to be_empty
+ end
+
+ context 'when `state` argument is `pending`' do
+ let(:state) { 'pending' }
+
+ it 'returns just the pending todo' do
+ post_graphql(query, current_user: current_user)
+
+ expect(todoable_response).to contain_exactly(
+ a_hash_including('id' => global_id_of(pending_todo))
+ )
+ end
+ end
+
+ context 'when `state` argument is `done`' do
+ let(:state) { 'done' }
+
+ it 'returns just the done todo' do
+ post_graphql(query, current_user: current_user)
+
+ expect(todoable_response).to contain_exactly(
+ a_hash_including('id' => global_id_of(done_todo))
+ )
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb
new file mode 100644
index 00000000000..84b2fd63d46
--- /dev/null
+++ b/spec/requests/api/graphql/group/group_members_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting group members information' do
+ include GraphqlHelpers
+
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_1) { create(:user, username: 'user') }
+ let_it_be(:user_2) { create(:user, username: 'test') }
+
+ let(:member_data) { graphql_data['group']['groupMembers']['edges'] }
+
+ before do
+ [user_1, user_2].each { |user| group.add_guest(user) }
+ end
+
+ context 'when the request is correct' do
+ it_behaves_like 'a working graphql query' do
+ before do
+ fetch_members(user)
+ end
+ end
+
+ it 'returns group members successfully' do
+ fetch_members(user)
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(user_1.to_global_id.to_s, user_2.to_global_id.to_s)
+ end
+
+ it 'returns members that match the search query' do
+ fetch_members(user, { search: 'test' })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(user_2.to_global_id.to_s)
+ end
+ end
+
+ def fetch_members(user = nil, args = {})
+ post_graphql(members_query(args), current_user: user)
+ end
+
+ def members_query(args = {})
+ members_node = <<~NODE
+ edges {
+ node {
+ user {
+ id
+ }
+ }
+ }
+ NODE
+
+ graphql_query_for("group",
+ { full_path: group.full_path },
+ [query_graphql_field("groupMembers", args, members_node)]
+ )
+ end
+
+ def expect_array_response(*items)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(member_data).to be_an Array
+ expect(member_data.map { |node| node["node"]["user"]["id"] }).to match_array(items)
+ end
+end
diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb
index d99bff2e349..83180c7d7a5 100644
--- a/spec/requests/api/graphql/group_query_spec.rb
+++ b/spec/requests/api/graphql/group_query_spec.rb
@@ -89,18 +89,13 @@ RSpec.describe 'getting group information', :do_not_mock_admin_mode do
end
it 'avoids N+1 queries' do
- control_count = ActiveRecord::QueryRecorder.new do
- post_graphql(group_query(group1), current_user: admin)
- end.count
+ pending('See: https://gitlab.com/gitlab-org/gitlab/-/issues/245272')
queries = [{ query: group_query(group1) },
{ query: group_query(group2) }]
- expect do
- post_multiplex(queries, current_user: admin)
- end.not_to exceed_query_limit(control_count)
-
- expect(graphql_errors).to contain_exactly(nil, nil)
+ expect { post_multiplex(queries, current_user: admin) }
+ .to issue_same_number_of_queries_as { post_graphql(group_query(group1), current_user: admin) }
end
end
diff --git a/spec/requests/api/graphql/instance_statistics_measurements_spec.rb b/spec/requests/api/graphql/instance_statistics_measurements_spec.rb
new file mode 100644
index 00000000000..b8cbe54534a
--- /dev/null
+++ b/spec/requests/api/graphql/instance_statistics_measurements_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'InstanceStatisticsMeasurements' do
+ include GraphqlHelpers
+
+ let(:current_user) { create(:user, :admin) }
+ let!(:instance_statistics_measurement_1) { create(:instance_statistics_measurement, :project_count, recorded_at: 20.days.ago, count: 5) }
+ let!(:instance_statistics_measurement_2) { create(:instance_statistics_measurement, :project_count, recorded_at: 10.days.ago, count: 10) }
+
+ let(:query) { graphql_query_for(:instanceStatisticsMeasurements, 'identifier: PROJECTS', 'nodes { count }') }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'returns measurement objects' do
+ expect(graphql_data.dig('instanceStatisticsMeasurements', 'nodes')).to eq([{ "count" => 10 }, { "count" => 5 }])
+ end
+end
diff --git a/spec/requests/api/graphql/issue/issue_spec.rb b/spec/requests/api/graphql/issue/issue_spec.rb
new file mode 100644
index 00000000000..1c9d6b25856
--- /dev/null
+++ b/spec/requests/api/graphql/issue/issue_spec.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.issue(id)' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:current_user) { create(:user) }
+
+ let(:issue_data) { graphql_data['issue'] }
+
+ let_it_be(:issue_params) { { 'id' => issue.to_global_id.to_s } }
+ let(:issue_fields) { all_graphql_fields_for('Issue'.classify) }
+
+ let(:query) do
+ graphql_query_for('issue', issue_params, issue_fields)
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ context 'when the user does not have access to the issue' do
+ it 'returns nil' do
+ project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
+
+ post_graphql(query)
+
+ expect(issue_data).to be nil
+ end
+ end
+
+ context 'when the user does have access' do
+ before do
+ project.add_guest(current_user)
+ end
+
+ it 'returns the issue' do
+ post_graphql(query, current_user: current_user)
+
+ expect(issue_data).to include(
+ 'title' => issue.title,
+ 'description' => issue.description
+ )
+ end
+
+ context 'selecting any single field' do
+ where(:field) do
+ scalar_fields_of('Issue').map { |name| [name] }
+ end
+
+ with_them do
+ it_behaves_like 'a working graphql query' do
+ let(:issue_fields) do
+ field
+ end
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it "returns the Issue and field #{params['field']}" do
+ expect(issue_data.keys).to eq([field])
+ end
+ end
+ end
+ end
+
+ context 'selecting multiple fields' do
+ let(:issue_fields) { %w(title description) }
+
+ it 'returns the Issue with the specified fields' do
+ post_graphql(query, current_user: current_user)
+
+ expect(issue_data.keys).to eq( %w(title description) )
+ expect(issue_data['title']).to eq(issue.title)
+ expect(issue_data['description']).to eq(issue.description)
+ end
+ end
+
+ context 'when passed a non-Issue gid' do
+ let(:mr) {create(:merge_request)}
+
+ it 'returns an error' do
+ gid = mr.to_global_id.to_s
+ issue_params['id'] = gid
+
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_errors).not_to be nil
+ expect(graphql_errors.first['message']).to eq("\"#{gid}\" does not represent an instance of Issue")
+ end
+ end
+ end
+
+ context 'when there is a confidential issue' do
+ let!(:confidential_issue) do
+ create(:issue, :confidential, project: project)
+ end
+
+ let(:issue_params) { { 'id' => confidential_issue.to_global_id.to_s } }
+
+ context 'when the user cannot see confidential issues' do
+ it 'returns nil ' do
+ post_graphql(query, current_user: current_user)
+
+ expect(issue_data).to be nil
+ end
+ end
+
+ context 'when the user can see confidential issues' do
+ it 'returns the confidential issue' do
+ project.add_developer(current_user)
+
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data.count).to eq(1)
+ expect(issue_data['confidential']).to be(true)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
index 456b0a5dea1..e01f59ee6a0 100644
--- a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
+++ b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Getting Metrics Dashboard' do
let_it_be(:current_user) { create(:user) }
let(:project) { create(:project) }
- let!(:environment) { create(:environment, project: project) }
+ let(:environment) { create(:environment, project: project) }
let(:query) do
graphql_query_for(
@@ -25,73 +25,156 @@ RSpec.describe 'Getting Metrics Dashboard' do
)
end
- context 'for anonymous user' do
+ context 'with metrics_dashboard_exhaustive_validations feature flag off' do
before do
- post_graphql(query, current_user: current_user)
+ stub_feature_flags(metrics_dashboard_exhaustive_validations: false)
end
- context 'requested dashboard is available' do
- let(:path) { 'config/prometheus/common_metrics.yml' }
+ context 'for anonymous user' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ context 'requested dashboard is available' do
+ let(:path) { 'config/prometheus/common_metrics.yml' }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns nil' do
+ dashboard = graphql_data.dig('project', 'environments', 'nodes')
+
+ expect(dashboard).to be_nil
+ end
+ end
+ end
+
+ context 'for user with developer access' do
+ before do
+ project.add_developer(current_user)
+ post_graphql(query, current_user: current_user)
+ end
+
+ context 'requested dashboard is available' do
+ let(:path) { 'config/prometheus/common_metrics.yml' }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns metrics dashboard' do
+ dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
+
+ expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil)
+ end
+
+ context 'invalid dashboard' do
+ let(:path) { '.gitlab/dashboards/metrics.yml' }
+ 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" => ["panel_groups: should be an array of panel_groups objects"])
+ end
+ end
+
+ context 'empty dashboard' do
+ let(:path) { '.gitlab/dashboards/metrics.yml' }
+ let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) }
+
+ it 'returns metrics dashboard' do
+ dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
- it_behaves_like 'a working graphql query'
+ 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
+
+ context 'requested dashboard can not be found' do
+ let(:path) { 'config/prometheus/i_am_not_here.yml' }
- it 'returns nil' do
- dashboard = graphql_data.dig('project', 'environments', 'nodes')
+ it_behaves_like 'a working graphql query'
- expect(dashboard).to be_nil
+ it 'returns nil' do
+ dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
+
+ expect(dashboard).to be_nil
+ end
end
end
end
- context 'for user with developer access' do
+ context 'with metrics_dashboard_exhaustive_validations feature flag on' do
before do
- project.add_developer(current_user)
- post_graphql(query, current_user: current_user)
+ stub_feature_flags(metrics_dashboard_exhaustive_validations: true)
end
- context 'requested dashboard is available' do
- let(:path) { 'config/prometheus/common_metrics.yml' }
+ context 'for anonymous user' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ context 'requested dashboard is available' do
+ let(:path) { 'config/prometheus/common_metrics.yml' }
+
+ it_behaves_like 'a working graphql query'
- it_behaves_like 'a working graphql query'
+ it 'returns nil' do
+ dashboard = graphql_data.dig('project', 'environments', 'nodes')
- it 'returns metrics dashboard' do
- dashboard = graphql_data.dig('project', 'environments', 'nodes')[0]['metricsDashboard']
+ expect(dashboard).to be_nil
+ end
+ end
+ end
- expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil)
+ context 'for user with developer access' do
+ before do
+ project.add_developer(current_user)
+ post_graphql(query, current_user: current_user)
end
- context 'invalid dashboard' do
- let(:path) { '.gitlab/dashboards/metrics.yml' }
- let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "---\ndashboard: 'test'" }) }
+ context 'requested dashboard is available' do
+ let(:path) { 'config/prometheus/common_metrics.yml' }
+
+ it_behaves_like 'a working graphql query'
it 'returns metrics dashboard' do
dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
- expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["panel_groups: should be an array of panel_groups objects"])
+ expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => nil)
end
- end
- context 'empty dashboard' do
- let(:path) { '.gitlab/dashboards/metrics.yml' }
- let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) }
+ context 'invalid dashboard' do
+ let(:path) { '.gitlab/dashboards/metrics.yml' }
+ 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')
+ 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: should be an array of panel_groups objects"])
+ expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["root is missing required keys: panel_groups"])
+ end
+ end
+
+ context 'empty dashboard' do
+ let(:path) { '.gitlab/dashboards/metrics.yml' }
+ let(:project) { create(:project, :repository, :custom_repo, namespace: current_user.namespace, files: { path => "" }) }
+
+ it 'returns metrics dashboard' do
+ dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
+
+ expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["root is missing required keys: dashboard, panel_groups"])
+ end
end
end
- end
- context 'requested dashboard can not be found' do
- let(:path) { 'config/prometheus/i_am_not_here.yml' }
+ context 'requested dashboard can not be found' do
+ let(:path) { 'config/prometheus/i_am_not_here.yml' }
- it_behaves_like 'a working graphql query'
+ it_behaves_like 'a working graphql query'
- it 'returns nil' do
- dashboard = graphql_data.dig('project', 'environments', 'nodes')[0]['metricsDashboard']
+ it 'returns nil' do
+ dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
- expect(dashboard).to be_nil
+ expect(dashboard).to be_nil
+ end
end
end
end
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 1891300dace..1d38bb39d59 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
@@ -32,9 +32,7 @@ RSpec.describe 'Adding an AwardEmoji' do
context 'when the user does not have permission' do
it_behaves_like 'a mutation that does not create an AwardEmoji'
-
- it_behaves_like 'a mutation that returns top-level errors',
- errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action']
+ it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when the user has permission' 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 665b511abb8..c6e8800de1f 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
@@ -33,9 +33,7 @@ RSpec.describe 'Removing an AwardEmoji' do
shared_examples 'a mutation that does not authorize the user' do
it_behaves_like 'a mutation that does not destroy an AwardEmoji'
-
- it_behaves_like 'a mutation that returns top-level errors',
- errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action']
+ it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when the current_user does not own the award emoji' do
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 ab4a213fde3..2df59ce97ca 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
@@ -143,8 +143,6 @@ RSpec.describe 'Toggling an AwardEmoji' do
context 'when the user does not have permission' do
it_behaves_like 'a mutation that does not create or destroy an AwardEmoji'
-
- it_behaves_like 'a mutation that returns top-level errors',
- errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action']
+ it_behaves_like 'a mutation that returns a top-level access error'
end
end
diff --git a/spec/requests/api/graphql/mutations/boards/destroy_spec.rb b/spec/requests/api/graphql/mutations/boards/destroy_spec.rb
new file mode 100644
index 00000000000..a6d894e698d
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/boards/destroy_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Boards::Destroy do
+ include GraphqlHelpers
+
+ let_it_be(:current_user, reload: true) { create(:user) }
+ let_it_be(:project, reload: true) { create(:project) }
+ let_it_be(:board) { create(:board, project: project) }
+ let_it_be(:other_board) { create(:board, project: project) }
+ let(:mutation) do
+ variables = {
+ id: GitlabSchema.id_from_object(board).to_s
+ }
+
+ graphql_mutation(:destroy_board, variables)
+ end
+
+ subject { post_graphql_mutation(mutation, current_user: current_user) }
+
+ def mutation_response
+ graphql_mutation_response(:destroy_board)
+ end
+
+ context 'when the user does not have permission' do
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not destroy the board' do
+ expect { subject }.not_to change { Board.count }
+ end
+ end
+
+ context 'when the user has permission' do
+ before do
+ project.add_maintainer(current_user)
+ end
+
+ context 'when given id is not for a board' do
+ let_it_be(:board) { build_stubbed(:issue, project: project) }
+
+ it 'returns an error' do
+ subject
+
+ expect(graphql_errors.first['message']).to include('does not represent an instance of Board')
+ end
+ end
+
+ context 'when everything is ok' do
+ it 'destroys the board' do
+ expect { subject }.to change { Board.count }.from(2).to(1)
+ end
+
+ it 'returns an empty board' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response).to have_key('board')
+ expect(mutation_response['board']).to be_nil
+ end
+ end
+
+ context 'when there is only 1 board for the parent' do
+ before do
+ other_board.destroy!
+ end
+
+ it 'does not destroy the board' do
+ expect { subject }.not_to change { Board.count }.from(1)
+ end
+
+ it 'returns an error and not nil board' do
+ subject
+
+ expect(mutation_response['errors']).not_to be_empty
+ expect(mutation_response['board']).not_to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb b/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb
new file mode 100644
index 00000000000..328f4fb7b6e
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Create a label or backlog board list' do
+ include GraphqlHelpers
+
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:board) { create(:board, group: group) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:dev_label) do
+ create(:group_label, title: 'Development', color: '#FFAABB', group: group)
+ end
+
+ let(:current_user) { user }
+ let(:mutation) { graphql_mutation(:board_list_create, input) }
+ let(:mutation_response) { graphql_mutation_response(:board_list_create) }
+
+ context 'the user is not allowed to read board lists' do
+ let(:input) { { board_id: board.to_global_id.to_s, backlog: true } }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when user has permissions to admin board lists' do
+ before do
+ group.add_reporter(current_user)
+ end
+
+ describe 'backlog list' do
+ let(:input) { { board_id: board.to_global_id.to_s, backlog: true } }
+
+ it 'creates the list' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['list'])
+ .to include('position' => nil, 'listType' => 'backlog')
+ end
+ end
+
+ describe 'label list' do
+ let(:input) { { board_id: board.to_global_id.to_s, label_id: dev_label.to_global_id.to_s } }
+
+ it 'creates the list' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['list'])
+ .to include('position' => 0, 'listType' => 'label', 'label' => include('title' => 'Development'))
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb b/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb
index 8a6d2cb3994..8e24e053211 100644
--- a/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/boards/lists/update_spec.rb
@@ -15,8 +15,7 @@ RSpec.describe 'Update of an existing board list' do
let(:mutation_response) { graphql_mutation_response(:update_board_list) }
context 'the user is not allowed to read board lists' do
- it_behaves_like 'a mutation that returns top-level errors',
- errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action']
+ it_behaves_like 'a mutation that returns a top-level access error'
end
before do
diff --git a/spec/requests/api/graphql/mutations/branches/create_spec.rb b/spec/requests/api/graphql/mutations/branches/create_spec.rb
index 082b445bf3e..fc09f57a389 100644
--- a/spec/requests/api/graphql/mutations/branches/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/branches/create_spec.rb
@@ -15,8 +15,7 @@ RSpec.describe 'Creation of a new branch' do
let(:mutation_response) { graphql_mutation_response(:create_branch) }
context 'the user is not allowed to create a branch' do
- it_behaves_like 'a mutation that returns top-level errors',
- errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action']
+ it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when user has permissions to create a branch' do
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb
new file mode 100644
index 00000000000..a20ac823550
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_cancel_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'PipelineCancel' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+
+ let(:mutation) do
+ variables = {
+ id: pipeline.to_global_id.to_s
+ }
+ graphql_mutation(:pipeline_cancel, variables, 'errors')
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:pipeline_cancel) }
+
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ it 'does not cancel any pipelines not owned by the current user' do
+ build = create(:ci_build, :running, pipeline: pipeline)
+
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).not_to be_empty
+ expect(build).not_to be_canceled
+ end
+
+ it 'returns a error if the pipline cannot be be canceled' do
+ build = create(:ci_build, :success, pipeline: pipeline)
+
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(mutation_response).to include('errors' => include(eq 'Pipeline is not cancelable'))
+ expect(build).not_to be_canceled
+ end
+
+ it "cancels all cancelable builds from a pipeline" do
+ build = create(:ci_build, :running, pipeline: pipeline)
+
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(build.reload).to be_canceled
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb
new file mode 100644
index 00000000000..08959d354e2
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_destroy_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'PipelineDestroy' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { project.owner }
+ let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project, user: user) }
+
+ let(:mutation) do
+ variables = {
+ id: pipeline.to_global_id.to_s
+ }
+ graphql_mutation(:pipeline_destroy, variables, 'errors')
+ end
+
+ it 'returns an error if the user is not allowed to destroy the pipeline' do
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).not_to be_empty
+ end
+
+ it 'destroys a pipeline' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb b/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb
new file mode 100644
index 00000000000..f6acf29c321
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/pipeline_retry_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'PipelineRetry' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+
+ let(:mutation) do
+ variables = {
+ id: pipeline.to_global_id.to_s
+ }
+ graphql_mutation(:pipeline_retry, variables,
+ <<-QL
+ errors
+ pipeline {
+ id
+ }
+ QL
+ )
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:pipeline_retry) }
+
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ it 'returns an error if the user is not allowed to retry the pipeline' do
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).not_to be_empty
+ end
+
+ it 'retries a pipeline' do
+ pipeline_id = ::Gitlab::GlobalId.build(pipeline, id: pipeline.id).to_s
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['pipeline']['id']).to eq(pipeline_id)
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/commits/create_spec.rb b/spec/requests/api/graphql/mutations/commits/create_spec.rb
index 9e4a96700bb..ac4fa7cfe83 100644
--- a/spec/requests/api/graphql/mutations/commits/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/commits/create_spec.rb
@@ -24,8 +24,7 @@ RSpec.describe 'Creation of a new commit' do
let(:mutation_response) { graphql_mutation_response(:commit_create) }
context 'the user is not allowed to create a commit' do
- it_behaves_like 'a mutation that returns top-level errors',
- errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action']
+ it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when user has permissions to create a commit' do
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 9a9c7107b20..2189ae3c519 100644
--- a/spec/requests/api/graphql/mutations/design_management/upload_spec.rb
+++ b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb
@@ -12,11 +12,11 @@ RSpec.describe "uploading designs" do
let(:files) { [fixture_file_upload("spec/fixtures/dk.png")] }
let(:variables) { {} }
- let(:mutation) do
+ def mutation
input = {
project_path: project.full_path,
iid: issue.iid,
- files: files
+ files: files.dup
}.merge(variables)
graphql_mutation(:design_management_upload, input)
end
@@ -30,31 +30,15 @@ RSpec.describe "uploading designs" do
end
it "returns an error if the user is not allowed to upload designs" do
- post_graphql_mutation(mutation, current_user: create(:user))
+ post_graphql_mutation_with_uploads(mutation, current_user: create(:user))
expect(graphql_errors).to be_present
end
- it "succeeds (backward compatibility)" do
- post_graphql_mutation(mutation, current_user: current_user)
+ it "succeeds, and responds with the created designs" do
+ post_graphql_mutation_with_uploads(mutation, current_user: current_user)
expect(graphql_errors).not_to be_present
- end
-
- it 'succeeds' do
- file_path_in_params = ['designManagementUploadInput', 'files', 0]
- params = mutation_to_apollo_uploads_param(mutation, files: [file_path_in_params])
-
- workhorse_post_with_file(api('/', current_user, version: 'graphql'),
- params: params,
- file_key: '1'
- )
-
- expect(graphql_errors).not_to be_present
- end
-
- it "responds with the created designs" do
- post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response).to include(
"designs" => a_collection_containing_exactly(
@@ -65,7 +49,7 @@ RSpec.describe "uploading designs" do
it "can respond with skipped designs" do
2.times do
- post_graphql_mutation(mutation, current_user: current_user)
+ post_graphql_mutation_with_uploads(mutation, current_user: current_user)
files.each(&:rewind)
end
@@ -80,7 +64,7 @@ RSpec.describe "uploading designs" do
let(:variables) { { iid: "123" } }
it "returns an error" do
- post_graphql_mutation(mutation, current_user: create(:user))
+ post_graphql_mutation_with_uploads(mutation, current_user: create(:user))
expect(graphql_errors).not_to be_empty
end
@@ -92,7 +76,7 @@ RSpec.describe "uploading designs" do
expect(service).to receive(:execute).and_return({ status: :error, message: "Something went wrong" })
end
- post_graphql_mutation(mutation, current_user: current_user)
+ post_graphql_mutation_with_uploads(mutation, current_user: current_user)
expect(mutation_response["errors"].first).to eq("Something went wrong")
end
end
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 457c37e900b..450996bf76b 100644
--- a/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb
+++ b/spec/requests/api/graphql/mutations/discussions/toggle_resolve_spec.rb
@@ -20,8 +20,7 @@ RSpec.describe 'Toggling the resolve status of a discussion' do
context 'when the user does not have permission' do
let_it_be(:current_user) { create(:user) }
- it_behaves_like 'a mutation that returns top-level errors',
- errors: ["The resource that you are attempting to access does not exist or you don't have permission to perform this action"]
+ it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when user has permission' do
diff --git a/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb b/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb
index f1d55430e02..4989d096925 100644
--- a/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_locked_spec.rb
@@ -32,12 +32,7 @@ RSpec.describe 'Setting an issue as locked' do
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
+ it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when user is allowed to update the issue' do
diff --git a/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb b/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb
new file mode 100644
index 00000000000..96fd2368765
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/issues/set_severity_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Setting severity level of an incident' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let(:incident) { create(:incident) }
+ let(:project) { incident.project }
+ let(:input) { { severity: 'CRITICAL' } }
+
+ let(:mutation) do
+ variables = {
+ project_path: project.full_path,
+ iid: incident.iid.to_s
+ }
+
+ graphql_mutation(:issue_set_severity, variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ issue {
+ iid
+ severity
+ }
+ QL
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:issue_set_severity)
+ end
+
+ context 'when the user is not allowed to update the incident' 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: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(graphql_errors).to include(a_hash_including('message' => error))
+ end
+ end
+
+ context 'when the user is allowed to update the incident' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'updates the issue' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response.dig('issue', 'severity')).to eq('CRITICAL')
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/issues/update_spec.rb b/spec/requests/api/graphql/mutations/issues/update_spec.rb
index fd983c683be..af52f9d57a3 100644
--- a/spec/requests/api/graphql/mutations/issues/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/update_spec.rb
@@ -20,8 +20,7 @@ RSpec.describe 'Update of an existing issue' do
let(:mutation_response) { graphql_mutation_response(:update_issue) }
context 'the user is not allowed to update issue' do
- it_behaves_like 'a mutation that returns top-level errors',
- errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action']
+ it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when user has permissions to update issue' do
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 9297ca054c7..bf759521dc0 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/create_spec.rb
@@ -24,8 +24,7 @@ RSpec.describe 'Creation of a new merge request' do
let(:mutation_response) { graphql_mutation_response(:merge_request_create) }
context 'the user is not allowed to create a branch' do
- it_behaves_like 'a mutation that returns top-level errors',
- errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action']
+ it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when user has permissions to create a merge request' do
diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
index 0e2da94f0f9..10ca2cf1cf8 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
@@ -101,7 +101,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do
graphql_mutation(:create_annotation, variables)
end
- it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab id.']
+ it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab ID.']
end
end
end
@@ -188,7 +188,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do
graphql_mutation(:create_annotation, variables)
end
- it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab id.']
+ it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab ID.']
end
end
diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
index 2459a6f3828..7357f3e1e35 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
@@ -45,7 +45,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete do
graphql_mutation(:delete_annotation, variables)
end
- it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab id.']
+ it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab ID.']
end
context 'when the delete fails' do
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 e847c46be1b..21da1332465 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
@@ -44,7 +44,7 @@ RSpec.describe 'Adding a DiffNote' do
it_behaves_like 'a Note mutation when there are active record validation errors', model: DiffNote
context do
- let(:diff_refs) { build(:merge_request).diff_refs } # Allow fake diff refs so arguments are valid
+ let(:diff_refs) { build(:commit).diff_refs } # Allow fake diff refs so arguments are valid
it_behaves_like 'a Note mutation when the given resource id is not for a Noteable'
end
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 896a398e308..8bc68e6017c 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
@@ -47,7 +47,7 @@ RSpec.describe 'Adding an image DiffNote' do
it_behaves_like 'a Note mutation when there are active record validation errors', model: DiffNote
context do
- let(:diff_refs) { build(:merge_request).diff_refs } # Allow fake diff refs so arguments are valid
+ let(:diff_refs) { build(:commit).diff_refs } # Allow fake diff refs so arguments are valid
it_behaves_like 'a Note mutation when the given resource id is not for a Noteable'
end
diff --git a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb
index 6002a5b5b9d..49f09fadfea 100644
--- a/spec/requests/api/graphql/mutations/notes/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/destroy_spec.rb
@@ -21,8 +21,7 @@ RSpec.describe 'Destroying a Note' do
context 'when the user does not have permission' do
let(:current_user) { create(:user) }
- it_behaves_like 'a mutation that returns top-level errors',
- errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action']
+ it_behaves_like 'a mutation that returns a top-level access error'
it 'does not destroy the Note' do
expect do
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 463a872d95d..0c00906d6bf 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
@@ -59,8 +59,7 @@ RSpec.describe 'Updating an image DiffNote' do
context 'when the user does not have permission' do
let_it_be(:current_user) { create(:user) }
- it_behaves_like 'a mutation that returns top-level errors',
- errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action']
+ it_behaves_like 'a mutation that returns a top-level access error'
it 'does not update the DiffNote' do
post_graphql_mutation(mutation, current_user: current_user)
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 0d93afe9434..5a92ffe61b8 100644
--- a/spec/requests/api/graphql/mutations/notes/update/note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/update/note_spec.rb
@@ -22,8 +22,7 @@ RSpec.describe 'Updating a Note' do
context 'when the user does not have permission' do
let_it_be(:current_user) { create(:user) }
- it_behaves_like 'a mutation that returns top-level errors',
- errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action']
+ it_behaves_like 'a mutation that returns a top-level access error'
it 'does not update the Note' do
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
index 56a5f4907c1..1bb446de708 100644
--- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
@@ -7,22 +7,24 @@ RSpec.describe 'Creating a Snippet' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
- let(:content) { 'Initial content' }
+
let(:description) { 'Initial description' }
let(:title) { 'Initial title' }
- let(:file_name) { 'Initial file_name' }
let(:visibility_level) { 'public' }
+ 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(:project_path) { nil }
let(:uploaded_files) { nil }
let(:mutation_vars) do
{
- content: content,
description: description,
visibility_level: visibility_level,
- file_name: file_name,
title: title,
project_path: project_path,
- uploaded_files: uploaded_files
+ uploaded_files: uploaded_files,
+ blob_actions: actions
}
end
@@ -62,24 +64,47 @@ RSpec.describe 'Creating a Snippet' do
context 'when the user has permission' do
let(:current_user) { user }
- context 'with PersonalSnippet' do
- it 'creates the Snippet' do
+ shared_examples 'does not create snippet' do
+ it 'does not create the Snippet' do
expect do
subject
- end.to change { Snippet.count }.by(1)
+ end.not_to change { Snippet.count }
end
- it 'returns the created Snippet' do
+ it 'does not return Snippet' do
subject
- expect(mutation_response['snippet']['blob']['richData']).to be_nil
- expect(mutation_response['snippet']['blob']['plainData']).to match(content)
+ expect(mutation_response['snippet']).to be_nil
+ end
+ end
+
+ shared_examples 'creates snippet' do
+ it 'returns the created Snippet' do
+ expect do
+ subject
+ end.to change { Snippet.count }.by(1)
+
expect(mutation_response['snippet']['title']).to eq(title)
expect(mutation_response['snippet']['description']).to eq(description)
- expect(mutation_response['snippet']['fileName']).to eq(file_name)
expect(mutation_response['snippet']['visibilityLevel']).to eq(visibility_level)
- expect(mutation_response['snippet']['project']).to be_nil
+ 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
+
+ it_behaves_like 'snippet edit usage data counters'
+ end
+
+ context 'with PersonalSnippet' do
+ it_behaves_like 'creates snippet'
end
context 'with ProjectSnippet' do
@@ -89,23 +114,7 @@ RSpec.describe 'Creating a Snippet' do
project.add_developer(current_user)
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']['blob']['richData']).to be_nil
- expect(mutation_response['snippet']['blob']['plainData']).to match(content)
- expect(mutation_response['snippet']['title']).to eq(title)
- expect(mutation_response['snippet']['description']).to eq(description)
- expect(mutation_response['snippet']['fileName']).to eq(file_name)
- expect(mutation_response['snippet']['visibilityLevel']).to eq(visibility_level)
- expect(mutation_response['snippet']['project']['fullPath']).to eq(project_path)
- end
+ it_behaves_like 'creates snippet'
context 'when the project path is invalid' do
let(:project_path) { 'foobar' }
@@ -122,61 +131,8 @@ RSpec.describe 'Creating a Snippet' do
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
- end
-
- shared_examples 'does not create snippet' do
- it 'does not create the Snippet' do
- expect do
- subject
- end.not_to change { Snippet.count }
- end
-
- it 'does not return Snippet' do
- subject
-
- expect(mutation_response['snippet']).to be_nil
- 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,
- blob_actions: 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
+ it_behaves_like 'snippet edit usage data counters'
end
context 'when there are ActiveRecord validation errors' do
@@ -187,7 +143,7 @@ RSpec.describe 'Creating a Snippet' do
end
context 'when there non ActiveRecord errors' do
- let(:file_name) { 'invalid://file/path' }
+ let(:file_1) { { filePath: 'invalid://file/path', content: 'foobar' }}
it_behaves_like 'a mutation that returns errors in the response', errors: ['Repository Error creating the snippet - Invalid file name']
it_behaves_like 'does not create snippet'
diff --git a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
index c861564c66b..b71f87d2702 100644
--- a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
@@ -56,7 +56,7 @@ RSpec.describe 'Destroying a Snippet' do
post_graphql_mutation(mutation, current_user: current_user)
expect(graphql_errors)
- .to include(a_hash_including('message' => "#{snippet_gid} is not a valid id for Snippet."))
+ .to include(a_hash_including('message' => "#{snippet_gid} is not a valid ID for Snippet."))
end
it 'does not destroy the Snippet' do
diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
index 3f39c0ab851..58ce74b9263 100644
--- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
@@ -12,18 +12,20 @@ RSpec.describe 'Updating a Snippet' do
let(:updated_content) { 'Updated content' }
let(:updated_description) { 'Updated description' }
let(:updated_title) { 'Updated_title' }
- let(:updated_file_name) { 'Updated file_name' }
let(:current_user) { snippet.author }
-
+ let(:updated_file) { 'CHANGELOG' }
+ let(:deleted_file) { 'README' }
let(:snippet_gid) { GitlabSchema.id_from_object(snippet).to_s }
let(:mutation_vars) do
{
id: snippet_gid,
- content: updated_content,
description: updated_description,
visibility_level: 'public',
- file_name: updated_file_name,
- title: updated_title
+ title: updated_title,
+ blob_actions: [
+ { action: :update, filePath: updated_file, content: updated_content },
+ { action: :delete, filePath: deleted_file }
+ ]
}
end
@@ -50,21 +52,32 @@ RSpec.describe 'Updating a Snippet' do
end
context 'when the user has permission' do
- it 'updates the Snippet' do
+ it 'updates the snippet record' do
post_graphql_mutation(mutation, current_user: current_user)
expect(snippet.reload.title).to eq(updated_title)
end
- it 'returns the updated Snippet' do
+ it 'updates the Snippet' do
+ blob_to_update = blob_at(updated_file)
+ blob_to_delete = blob_at(deleted_file)
+
+ expect(blob_to_update.data).not_to eq updated_content
+ expect(blob_to_delete).to be_present
+
post_graphql_mutation(mutation, current_user: current_user)
- expect(mutation_response['snippet']['blob']['richData']).to be_nil
- expect(mutation_response['snippet']['blob']['plainData']).to match(updated_content)
- expect(mutation_response['snippet']['title']).to eq(updated_title)
- expect(mutation_response['snippet']['description']).to eq(updated_description)
- expect(mutation_response['snippet']['fileName']).to eq(updated_file_name)
- expect(mutation_response['snippet']['visibilityLevel']).to eq('public')
+ blob_to_update = blob_at(updated_file)
+ blob_to_delete = blob_at(deleted_file)
+
+ aggregate_failures do
+ expect(blob_to_update.data).to eq updated_content
+ expect(blob_to_delete).to be_nil
+ expect(blob_in_mutation_response(updated_file)['plainData']).to match(updated_content)
+ expect(mutation_response['snippet']['title']).to eq(updated_title)
+ expect(mutation_response['snippet']['description']).to eq(updated_description)
+ expect(mutation_response['snippet']['visibilityLevel']).to eq('public')
+ end
end
context 'when there are ActiveRecord validation errors' do
@@ -79,16 +92,29 @@ RSpec.describe 'Updating a Snippet' do
end
it 'returns the Snippet with its original values' do
+ blob_to_update = blob_at(updated_file)
+ blob_to_delete = blob_at(deleted_file)
+
post_graphql_mutation(mutation, current_user: current_user)
- expect(mutation_response['snippet']['blob']['richData']).to be_nil
- expect(mutation_response['snippet']['blob']['plainData']).to match(original_content)
- expect(mutation_response['snippet']['title']).to eq(original_title)
- expect(mutation_response['snippet']['description']).to eq(original_description)
- expect(mutation_response['snippet']['fileName']).to eq(original_file_name)
- expect(mutation_response['snippet']['visibilityLevel']).to eq('private')
+ aggregate_failures do
+ expect(blob_at(updated_file).data).to eq blob_to_update.data
+ expect(blob_at(deleted_file).data).to eq blob_to_delete.data
+ expect(blob_in_mutation_response(deleted_file)['plainData']).not_to be_nil
+ expect(mutation_response['snippet']['title']).to eq(original_title)
+ expect(mutation_response['snippet']['description']).to eq(original_description)
+ expect(mutation_response['snippet']['visibilityLevel']).to eq('private')
+ end
end
end
+
+ def blob_in_mutation_response(filename)
+ mutation_response['snippet']['blobs'].select { |blob| blob['name'] == filename }[0]
+ end
+
+ def blob_at(filename)
+ snippet.repository.blob_at('HEAD', filename)
+ end
end
end
@@ -96,6 +122,7 @@ RSpec.describe 'Updating a Snippet' do
let(:snippet) do
create(:personal_snippet,
:private,
+ :repository,
file_name: original_file_name,
title: original_title,
content: original_content,
@@ -104,6 +131,7 @@ RSpec.describe 'Updating a Snippet' do
it_behaves_like 'graphql update actions'
it_behaves_like 'when the snippet is not found'
+ it_behaves_like 'snippet edit usage data counters'
end
describe 'ProjectSnippet' do
@@ -111,6 +139,7 @@ RSpec.describe 'Updating a Snippet' do
let(:snippet) do
create(:project_snippet,
:private,
+ :repository,
project: project,
author: create(:user),
file_name: original_file_name,
@@ -145,44 +174,10 @@ RSpec.describe 'Updating a Snippet' do
expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
- end
-
- 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,
- blob_actions: [
- { 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
+ it_behaves_like 'snippet edit usage data counters'
end
- def blob_at(filename)
- snippet.repository.blob_at('HEAD', filename)
- end
+ it_behaves_like 'when the snippet is not found'
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 ed5552f3e30..705ef28ffd4 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
@@ -59,7 +59,6 @@ RSpec.describe 'Marking all todos done' do
context 'when user is not logged in' do
let(:current_user) { nil }
- it_behaves_like 'a mutation that returns top-level errors',
- errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action']
+ it_behaves_like 'a mutation that returns a top-level access error'
end
end
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 9c4733f6769..8bf8b96aff5 100644
--- a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb
@@ -63,14 +63,11 @@ RSpec.describe 'Marking todos done' do
context 'when todo does not belong to requesting user' do
let(:input) { { id: other_user_todo.to_global_id.to_s } }
- let(:access_error) { 'The resource that you are attempting to access does not exist or you don\'t have permission to perform this action' }
- it 'contains the expected error' do
- post_graphql_mutation(mutation, current_user: current_user)
+ it_behaves_like 'a mutation that returns a top-level access error'
- errors = json_response['errors']
- expect(errors).not_to be_blank
- expect(errors.first['message']).to eq(access_error)
+ it 'results in the correct todo states' do
+ post_graphql_mutation(mutation, current_user: current_user)
expect(todo1.reload.state).to eq('pending')
expect(todo2.reload.state).to eq('done')
@@ -80,7 +77,7 @@ RSpec.describe 'Marking todos done' do
context 'when using an invalid gid' do
let(:input) { { id: 'invalid_gid' } }
- let(:invalid_gid_error) { 'invalid_gid is not a valid GitLab id.' }
+ let(:invalid_gid_error) { 'invalid_gid is not a valid GitLab ID.' }
it 'contains the expected error' do
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/requests/api/graphql/mutations/todos/restore_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_spec.rb
index 6dedde56e13..8451dcdf587 100644
--- a/spec/requests/api/graphql/mutations/todos/restore_spec.rb
+++ b/spec/requests/api/graphql/mutations/todos/restore_spec.rb
@@ -63,14 +63,11 @@ RSpec.describe 'Restoring Todos' do
context 'when todo does not belong to requesting user' do
let(:input) { { id: other_user_todo.to_global_id.to_s } }
- let(:access_error) { 'The resource that you are attempting to access does not exist or you don\'t have permission to perform this action' }
- it 'contains the expected error' do
- post_graphql_mutation(mutation, current_user: current_user)
+ it_behaves_like 'a mutation that returns a top-level access error'
- errors = json_response['errors']
- expect(errors).not_to be_blank
- expect(errors.first['message']).to eq(access_error)
+ it 'results in the correct todo states' do
+ post_graphql_mutation(mutation, current_user: current_user)
expect(todo1.reload.state).to eq('done')
expect(todo2.reload.state).to eq('pending')
@@ -80,7 +77,7 @@ RSpec.describe 'Restoring Todos' do
context 'when using an invalid gid' do
let(:input) { { id: 'invalid_gid' } }
- let(:invalid_gid_error) { 'invalid_gid is not a valid GitLab id.' }
+ let(:invalid_gid_error) { 'invalid_gid is not a valid GitLab ID.' }
it 'contains the expected error' do
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb
index 0b634e6b689..03160719389 100644
--- a/spec/requests/api/graphql/namespace/projects_spec.rb
+++ b/spec/requests/api/graphql/namespace/projects_spec.rb
@@ -78,4 +78,43 @@ RSpec.describe 'getting projects' do
it_behaves_like 'a graphql namespace'
end
+
+ describe 'sorting and pagination' do
+ let(:data_path) { [:namespace, :projects] }
+
+ def pagination_query(params, page_info)
+ graphql_query_for(
+ 'namespace',
+ { 'fullPath' => subject.full_path },
+ <<~QUERY
+ projects(includeSubgroups: #{include_subgroups}, search: "#{search}", #{params}) {
+ #{page_info} edges {
+ node {
+ #{all_graphql_fields_for('Project')}
+ }
+ }
+ }
+ QUERY
+ )
+ end
+
+ def pagination_results_data(data)
+ data.map { |project| project.dig('node', 'name') }
+ end
+
+ context 'when sorting by similarity' do
+ let!(:project_1) { create(:project, name: 'Project', path: 'project', namespace: subject) }
+ let!(:project_2) { create(:project, name: 'Test Project', path: 'test-project', namespace: subject) }
+ let!(:project_3) { create(:project, name: 'Test', path: 'test', namespace: subject) }
+ let!(:project_4) { create(:project, name: 'Test Project Other', path: 'other-test-project', namespace: subject) }
+ let(:search) { 'test' }
+ let(:current_user) { user }
+
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { 'SIMILARITY' }
+ let(:first_param) { 2 }
+ let(:expected_results) { [project_3.name, project_2.name, project_4.name] }
+ end
+ end
+ end
end
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 ae5c8363d0f..65191e057c7 100644
--- a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
+++ b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
@@ -14,8 +14,6 @@ RSpec.describe 'Getting designs related to an issue' do
before do
enable_design_management
-
- note
end
it_behaves_like 'a working graphql query' do
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 06e613a09bc..5d4276f47ca 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -5,12 +5,13 @@ require 'spec_helper'
RSpec.describe 'getting an issue list for a project' do
include GraphqlHelpers
- let(:project) { create(:project, :repository, :public) }
- let(:current_user) { create(:user) }
let(:issues_data) { graphql_data['project']['issues']['edges'] }
- let!(:issues) do
+
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:issues, reload: true) do
[create(:issue, project: project, discussion_locked: true),
- create(:issue, project: project)]
+ create(:issue, :with_alert, project: project)]
end
let(:fields) do
@@ -85,7 +86,7 @@ RSpec.describe 'getting an issue list for a project' do
end
context 'when there is a confidential issue' do
- let!(:confidential_issue) do
+ let_it_be(:confidential_issue) do
create(:issue, :confidential, project: project)
end
@@ -256,9 +257,140 @@ RSpec.describe 'getting an issue list for a project' do
end
end
- def grab_iids(data = issues_data)
- data.map do |issue|
- issue.dig('node', 'iid').to_i
+ context 'fetching alert management alert' do
+ let(:fields) do
+ <<~QUERY
+ edges {
+ node {
+ iid
+ alertManagementAlert {
+ title
+ }
+ }
+ }
+ QUERY
+ end
+
+ # Alerts need to have developer permission and above
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'avoids N+1 queries' do
+ control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
+
+ create(:alert_management_alert, :with_issue, project: project )
+
+ expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
+ end
+
+ it 'returns the alert data' do
+ post_graphql(query, current_user: current_user)
+
+ alert_titles = issues_data.map { |issue| issue.dig('node', 'alertManagementAlert', 'title') }
+ expected_titles = issues.map { |issue| issue.alert_management_alert&.title }
+
+ expect(alert_titles).to contain_exactly(*expected_titles)
+ end
+ end
+
+ context 'fetching labels' do
+ let(:fields) do
+ <<~QUERY
+ edges {
+ node {
+ id
+ labels {
+ nodes {
+ id
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ before do
+ issues.each do |issue|
+ # create a label for each issue we have to properly test N+1
+ label = create(:label, project: project)
+ issue.update!(labels: [label])
+ end
+ end
+
+ def response_label_ids(response_data)
+ response_data.map do |edge|
+ edge['node']['labels']['nodes'].map { |u| u['id'] }
+ end.flatten
+ end
+
+ def labels_as_global_ids(issues)
+ issues.map(&:labels).flatten.map(&:to_global_id).map(&:to_s)
+ end
+
+ it 'avoids N+1 queries', :aggregate_failures do
+ control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
+ expect(issues_data.count).to eq(2)
+ expect(response_label_ids(issues_data)).to match_array(labels_as_global_ids(issues))
+
+ new_issues = issues + [create(:issue, project: project, labels: [create(:label, project: project)])]
+
+ expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
+ # graphql_data is memoized (see spec/support/helpers/graphql_helpers.rb)
+ # so we have to parse the body ourselves the second time
+ issues_data = Gitlab::Json.parse(response.body)['data']['project']['issues']['edges']
+ expect(issues_data.count).to eq(3)
+ expect(response_label_ids(issues_data)).to match_array(labels_as_global_ids(new_issues))
+ end
+ end
+
+ context 'fetching assignees' do
+ let(:fields) do
+ <<~QUERY
+ edges {
+ node {
+ id
+ assignees {
+ nodes {
+ id
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ before do
+ issues.each do |issue|
+ # create an assignee for each issue we have to properly test N+1
+ assignee = create(:user)
+ issue.update!(assignees: [assignee])
+ end
+ end
+
+ def response_assignee_ids(response_data)
+ response_data.map do |edge|
+ edge['node']['assignees']['nodes'].map { |node| node['id'] }
+ end.flatten
+ end
+
+ def assignees_as_global_ids(issues)
+ issues.map(&:assignees).flatten.map(&:to_global_id).map(&:to_s)
+ end
+
+ it 'avoids N+1 queries', :aggregate_failures do
+ control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
+ expect(issues_data.count).to eq(2)
+ expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(issues))
+
+ new_issues = issues + [create(:issue, project: project, assignees: [create(:user)])]
+
+ expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
+ # graphql_data is memoized (see spec/support/helpers/graphql_helpers.rb)
+ # so we have to parse the body ourselves the second time
+ issues_data = Gitlab::Json.parse(response.body)['data']['project']['issues']['edges']
+ expect(issues_data.count).to eq(3)
+ expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(new_issues))
end
end
end
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
index c39358a2db1..fae52fe814d 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -124,7 +124,8 @@ RSpec.describe 'getting merge request information nested in a project' do
'removeSourceBranch' => false,
'cherryPickOnCurrentMergeRequest' => false,
'revertOnCurrentMergeRequest' => false,
- 'updateMergeRequest' => false
+ 'updateMergeRequest' => false,
+ 'canMerge' => false
}
post_graphql(query, current_user: current_user)
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index bb63a5994b0..22b003501a1 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:current_user) { create(:user) }
- let_it_be(:label) { create(:label) }
+ let_it_be(:label) { create(:label, project: project) }
let_it_be(:merge_request_a) { create(:labeled_merge_request, :unique_branches, source_project: project, labels: [label]) }
let_it_be(:merge_request_b) { create(:merge_request, :closed, :unique_branches, source_project: project) }
let_it_be(:merge_request_c) { create(:labeled_merge_request, :closed, :unique_branches, source_project: project, labels: [label]) }
@@ -210,4 +210,48 @@ RSpec.describe 'getting merge request listings nested in a project' do
include_examples 'N+1 query check'
end
end
+ describe 'sorting and pagination' do
+ let(:data_path) { [:project, :mergeRequests] }
+
+ def pagination_query(params, page_info)
+ graphql_query_for(
+ :project,
+ { full_path: project.full_path },
+ <<~QUERY
+ mergeRequests(#{params}) {
+ #{page_info} edges {
+ node {
+ id
+ }
+ }
+ }
+ QUERY
+ )
+ end
+
+ def pagination_results_data(data)
+ data.map { |project| project.dig('node', 'id') }
+ end
+
+ context 'when sorting by merged_at DESC' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { 'MERGED_AT_DESC' }
+ let(:first_param) { 2 }
+
+ let(:expected_results) do
+ [
+ merge_request_b,
+ merge_request_c,
+ merge_request_d,
+ merge_request_a
+ ].map(&:to_gid).map(&:to_s)
+ end
+
+ before do
+ merge_request_c.metrics.update!(merged_at: 5.days.ago)
+ merge_request_b.metrics.update!(merged_at: 1.day.ago)
+ end
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb
index f9c19d9747d..8fce29d0dc6 100644
--- a/spec/requests/api/graphql/project/release_spec.rb
+++ b/spec/requests/api/graphql/project/release_spec.rb
@@ -10,6 +10,8 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let_it_be(:guest) { create(:user) }
let_it_be(:reporter) { create(:user) }
let_it_be(:stranger) { create(:user) }
+ let_it_be(:link_filepath) { '/direct/asset/link/path' }
+ let_it_be(:released_at) { Time.now - 1.day }
let(:params_for_issues_and_mrs) { { scope: 'all', state: 'opened', release_tag: release.tag } }
let(:post_query) { post_graphql(query, current_user: current_user) }
@@ -38,6 +40,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
name
createdAt
releasedAt
+ upcomingRelease
})
end
@@ -53,7 +56,8 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
'descriptionHtml' => release.description_html,
'name' => release.name,
'createdAt' => release.created_at.iso8601,
- 'releasedAt' => release.released_at.iso8601
+ 'releasedAt' => release.released_at.iso8601,
+ 'upcomingRelease' => false
})
end
end
@@ -127,7 +131,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let(:release_fields) do
query_graphql_field(:assets, nil,
- query_graphql_field(:links, nil, 'nodes { id name url external }'))
+ query_graphql_field(:links, nil, 'nodes { id name url external, directAssetUrl }'))
end
it 'finds all release links' do
@@ -138,7 +142,8 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
'id' => global_id_of(link),
'name' => link.name,
'url' => link.url,
- 'external' => link.external?
+ 'external' => link.external?,
+ 'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << link.filepath : link.url
}
end
@@ -268,9 +273,9 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' 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) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2], released_at: released_at) }
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(:release_link_2) { create(:release_link, release: release, filepath: link_filepath) }
before_all do
project.add_developer(developer)
@@ -309,9 +314,9 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' 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) { create(:release, :with_evidence, project: project, milestones: [milestone_1, milestone_2], released_at: released_at) }
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(:release_link_2) { create(:release_link, release: release, filepath: link_filepath) }
before_all do
project.add_developer(developer)
@@ -371,4 +376,45 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
it_behaves_like 'no access to the release field'
end
end
+
+ describe 'upcoming release' do
+ let(:path) { path_prefix }
+ let(:project) { create(:project, :repository, :private) }
+ let(:release) { create(:release, :with_evidence, project: project, released_at: released_at) }
+ let(:current_user) { developer }
+
+ let(:release_fields) do
+ query_graphql_field(%{
+ releasedAt
+ upcomingRelease
+ })
+ end
+
+ before do
+ project.add_developer(developer)
+ post_query
+ end
+
+ context 'future release' do
+ let(:released_at) { Time.now + 1.day }
+
+ it 'finds all release data' do
+ expect(data).to eq({
+ 'releasedAt' => release.released_at.iso8601,
+ 'upcomingRelease' => true
+ })
+ end
+ end
+
+ context 'past release' do
+ let(:released_at) { Time.now - 1.day }
+
+ it 'finds all release data' do
+ expect(data).to eq({
+ 'releasedAt' => release.released_at.iso8601,
+ 'upcomingRelease' => false
+ })
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/releases_spec.rb b/spec/requests/api/graphql/project/releases_spec.rb
index 7e418bbaa5b..7c57c0e9177 100644
--- a/spec/requests/api/graphql/project/releases_spec.rb
+++ b/spec/requests/api/graphql/project/releases_spec.rb
@@ -14,6 +14,7 @@ RSpec.describe 'Query.project(fullPath).releases()' do
graphql_query_for(:project, { fullPath: project.full_path },
%{
releases {
+ count
nodes {
tagName
tagPath
@@ -53,6 +54,20 @@ RSpec.describe 'Query.project(fullPath).releases()' do
stub_default_url_options(host: 'www.example.com')
end
+ shared_examples 'correct total count' do
+ let(:data) { graphql_data.dig('project', 'releases') }
+
+ before do
+ create_list(:release, 2, project: project)
+
+ post_query
+ end
+
+ it 'returns the total count' do
+ expect(data['count']).to eq(project.releases.count)
+ end
+ end
+
shared_examples 'full access to all repository-related fields' do
describe 'repository-related fields' do
before do
@@ -92,6 +107,8 @@ RSpec.describe 'Query.project(fullPath).releases()' do
)
end
end
+
+ it_behaves_like 'correct total count'
end
shared_examples 'no access to any repository-related fields' do
@@ -119,6 +136,8 @@ RSpec.describe 'Query.project(fullPath).releases()' do
)
end
end
+
+ it_behaves_like 'correct total count'
end
# editUrl is tested separately becuase its permissions
diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb
index c6049e098be..4b8ffb0675c 100644
--- a/spec/requests/api/graphql/project_query_spec.rb
+++ b/spec/requests/api/graphql/project_query_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe 'getting project information' do
include GraphqlHelpers
- let(:project) { create(:project, :repository) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, :repository, group: group) }
let(:current_user) { create(:user) }
let(:query) do
@@ -60,6 +61,51 @@ RSpec.describe 'getting project information' do
expect(graphql_data['project']['pipelines']['edges'].size).to eq(1)
end
end
+
+ it 'includes inherited members in project_members' do
+ group_member = create(:group_member, group: group)
+ project_member = create(:project_member, project: project)
+ member_query = <<~GQL
+ query {
+ project(fullPath: "#{project.full_path}") {
+ projectMembers {
+ nodes {
+ id
+ user {
+ username
+ }
+ ... on ProjectMember {
+ project {
+ id
+ }
+ }
+ ... on GroupMember {
+ group {
+ id
+ }
+ }
+ }
+ }
+ }
+ }
+ GQL
+
+ post_graphql(member_query, current_user: current_user)
+
+ member_ids = graphql_data.dig('project', 'projectMembers', 'nodes')
+ expect(member_ids).to include(
+ a_hash_including(
+ 'id' => group_member.to_global_id.to_s,
+ 'group' => { 'id' => group.to_global_id.to_s }
+ )
+ )
+ expect(member_ids).to include(
+ a_hash_including(
+ 'id' => project_member.to_global_id.to_s,
+ 'project' => { 'id' => project.to_global_id.to_s }
+ )
+ )
+ end
end
describe 'performance' do
diff --git a/spec/requests/api/graphql/user/starred_projects_query_spec.rb b/spec/requests/api/graphql/user/starred_projects_query_spec.rb
new file mode 100644
index 00000000000..8a1bd3d172f
--- /dev/null
+++ b/spec/requests/api/graphql/user/starred_projects_query_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Getting starredProjects of the user' do
+ include GraphqlHelpers
+
+ let(:query) do
+ graphql_query_for(:user, user_params, user_fields)
+ end
+
+ let(:user_params) { { username: user.username } }
+
+ let_it_be(:project_a) { create(:project, :public) }
+ let_it_be(:project_b) { create(:project, :private) }
+ let_it_be(:project_c) { create(:project, :private) }
+ let_it_be(:user, reload: true) { create(:user) }
+
+ let(:user_fields) { 'starredProjects { nodes { id } }' }
+ let(:starred_projects) { graphql_data_at(:user, :starred_projects, :nodes) }
+
+ before do
+ project_b.add_reporter(user)
+ project_c.add_reporter(user)
+
+ user.toggle_star(project_a)
+ user.toggle_star(project_b)
+ user.toggle_star(project_c)
+
+ post_graphql(query)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'found only public project' do
+ expect(starred_projects).to contain_exactly(
+ a_hash_including('id' => global_id_of(project_a))
+ )
+ end
+
+ context 'the current user is the user' do
+ let(:current_user) { user }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'found all projects' do
+ expect(starred_projects).to contain_exactly(
+ a_hash_including('id' => global_id_of(project_a)),
+ a_hash_including('id' => global_id_of(project_b)),
+ a_hash_including('id' => global_id_of(project_c))
+ )
+ end
+ end
+
+ context 'the current user is a member of a private project the user starred' do
+ let_it_be(:other_user) { create(:user) }
+
+ before do
+ project_b.add_reporter(other_user)
+
+ post_graphql(query, current_user: other_user)
+ end
+
+ it 'finds public and member projects' do
+ expect(starred_projects).to contain_exactly(
+ a_hash_including('id' => global_id_of(project_a)),
+ a_hash_including('id' => global_id_of(project_b))
+ )
+ end
+ end
+end
diff --git a/spec/requests/api/group_packages_spec.rb b/spec/requests/api/group_packages_spec.rb
index e02f6099637..f67cafbd8f5 100644
--- a/spec/requests/api/group_packages_spec.rb
+++ b/spec/requests/api/group_packages_spec.rb
@@ -141,5 +141,7 @@ RSpec.describe API::GroupPackages do
it_behaves_like 'returning response status', :bad_request
end
+
+ it_behaves_like 'does not cause n^2 queries'
end
end
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 1fa705423d2..9c0ea14e3e3 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe API::Helpers do
end
def error!(message, status, header)
- raise Exception.new("#{status} - #{message}")
+ raise StandardError.new("#{status} - #{message}")
end
def set_param(key, value)
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index 873189af397..4a0a7c81781 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -415,7 +415,7 @@ RSpec.describe API::Internal::Base do
let(:env) { {} }
around do |example|
- Timecop.freeze { example.run }
+ freeze_time { example.run }
end
before do
@@ -1179,7 +1179,7 @@ RSpec.describe API::Internal::Base do
let(:gl_repository) { "snippet-#{personal_snippet.id}" }
it 'does not try to notify that project moved' do
- allow(Gitlab::GlRepository).to receive(:parse).and_return([personal_snippet, nil, Gitlab::GlRepository::PROJECT])
+ allow(Gitlab::GlRepository).to receive(:parse).and_return([personal_snippet, nil, Gitlab::GlRepository::SNIPPET])
expect(Gitlab::Checks::ProjectMoved).not_to receive(:fetch_message)
diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb
index 555ca441fe7..f669483b5a4 100644
--- a/spec/requests/api/internal/kubernetes_spec.rb
+++ b/spec/requests/api/internal/kubernetes_spec.rb
@@ -3,24 +3,97 @@
require 'spec_helper'
RSpec.describe API::Internal::Kubernetes do
- describe "GET /internal/kubernetes/agent_info" do
+ let(:jwt_auth_headers) do
+ jwt_token = JWT.encode({ 'iss' => Gitlab::Kas::JWT_ISSUER }, Gitlab::Kas.secret, 'HS256')
+
+ { Gitlab::Kas::INTERNAL_API_REQUEST_HEADER => jwt_token }
+ end
+
+ let(:jwt_secret) { SecureRandom.random_bytes(Gitlab::Kas::SECRET_LENGTH) }
+
+ before do
+ allow(Gitlab::Kas).to receive(:secret).and_return(jwt_secret)
+ end
+
+ shared_examples 'authorization' do
+ context 'not authenticated' do
+ it 'returns 401' do
+ send_request(headers: { Gitlab::Kas::INTERNAL_API_REQUEST_HEADER => '' })
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
context 'kubernetes_agent_internal_api feature flag disabled' do
before do
stub_feature_flags(kubernetes_agent_internal_api: false)
end
it 'returns 404' do
- get api('/internal/kubernetes/agent_info')
+ send_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
+ end
+ shared_examples 'agent authentication' do
it 'returns 403 if Authorization header not sent' do
- get api('/internal/kubernetes/agent_info')
+ send_request
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ it 'returns 403 if Authorization is for non-existent agent' do
+ send_request(headers: { 'Authorization' => 'Bearer NONEXISTENT' })
expect(response).to have_gitlab_http_status(:forbidden)
end
+ end
+
+ describe 'POST /internal/kubernetes/usage_metrics' do
+ def send_request(headers: {}, params: {})
+ post api('/internal/kubernetes/usage_metrics'), params: params, headers: headers.reverse_merge(jwt_auth_headers)
+ end
+
+ include_examples 'authorization'
+
+ context 'is authenticated for an agent' do
+ let!(:agent_token) { create(:cluster_agent_token) }
+
+ it 'returns no_content for valid gitops_sync_count' do
+ send_request(params: { gitops_sync_count: 10 }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+
+ it 'returns no_content 0 gitops_sync_count' do
+ send_request(params: { gitops_sync_count: 0 }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+
+ it 'returns 400 for non number' do
+ send_request(params: { gitops_sync_count: 'string' }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'returns 400 for negative number' do
+ send_request(params: { gitops_sync_count: '-1' }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+ end
+
+ describe "GET /internal/kubernetes/agent_info" do
+ def send_request(headers: {}, params: {})
+ get api('/internal/kubernetes/agent_info'), params: params, headers: headers.reverse_merge(jwt_auth_headers)
+ end
+
+ include_examples 'authorization'
+ include_examples 'agent authentication'
context 'an agent is found' do
let!(:agent_token) { create(:cluster_agent_token) }
@@ -29,7 +102,7 @@ RSpec.describe API::Internal::Kubernetes do
let(:project) { agent.project }
it 'returns expected data', :aggregate_failures do
- get api('/internal/kubernetes/agent_info'), headers: { 'Authorization' => "Bearer #{agent_token.token}" }
+ send_request(headers: { 'Authorization' => "Bearer #{agent_token.token}" })
expect(response).to have_gitlab_http_status(:success)
@@ -53,42 +126,15 @@ RSpec.describe API::Internal::Kubernetes do
)
end
end
-
- context 'no such agent exists' do
- it 'returns 404' do
- get api('/internal/kubernetes/agent_info'), headers: { 'Authorization' => 'Bearer ABCD' }
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
end
describe 'GET /internal/kubernetes/project_info' do
- context 'kubernetes_agent_internal_api feature flag disabled' do
- before do
- stub_feature_flags(kubernetes_agent_internal_api: false)
- end
-
- it 'returns 404' do
- get api('/internal/kubernetes/project_info')
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ def send_request(headers: {}, params: {})
+ get api('/internal/kubernetes/project_info'), params: params, headers: headers.reverse_merge(jwt_auth_headers)
end
- it 'returns 403 if Authorization header not sent' do
- get api('/internal/kubernetes/project_info')
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
-
- context 'no such agent exists' do
- it 'returns 404' do
- get api('/internal/kubernetes/project_info'), headers: { 'Authorization' => 'Bearer ABCD' }
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
+ include_examples 'authorization'
+ include_examples 'agent authentication'
context 'an agent is found' do
let!(:agent_token) { create(:cluster_agent_token) }
@@ -99,7 +145,7 @@ RSpec.describe API::Internal::Kubernetes do
let(:project) { create(:project, :public) }
it 'returns expected data', :aggregate_failures do
- get api('/internal/kubernetes/project_info'), params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }
+ send_request(params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
expect(response).to have_gitlab_http_status(:success)
@@ -126,7 +172,7 @@ RSpec.describe API::Internal::Kubernetes do
let(:project) { create(:project, :private) }
it 'returns 404' do
- get api('/internal/kubernetes/project_info'), params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }
+ send_request(params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -136,7 +182,7 @@ RSpec.describe API::Internal::Kubernetes do
let(:project) { create(:project, :internal) }
it 'returns 404' do
- get api('/internal/kubernetes/project_info'), params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }
+ send_request(params: { id: project.id }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -144,7 +190,7 @@ RSpec.describe API::Internal::Kubernetes do
context 'project does not exist' do
it 'returns 404' do
- get api('/internal/kubernetes/project_info'), params: { id: 0 }, headers: { 'Authorization' => "Bearer #{agent_token.token}" }
+ send_request(params: { id: 0 }, headers: { 'Authorization' => "Bearer #{agent_token.token}" })
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/requests/api/issue_links_spec.rb b/spec/requests/api/issue_links_spec.rb
new file mode 100644
index 00000000000..a4243766111
--- /dev/null
+++ b/spec/requests/api/issue_links_spec.rb
@@ -0,0 +1,207 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::IssueLinks do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ before do
+ project.add_guest(user)
+ end
+
+ describe 'GET /links' do
+ context 'when unauthenticated' do
+ it 'returns 401' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/links")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns related issues' do
+ target_issue = create(:issue, project: project)
+ create(:issue_link, source: issue, target: target_issue)
+
+ get api("/projects/#{project.id}/issues/#{issue.iid}/links", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(response).to match_response_schema('public_api/v4/issue_links')
+ end
+ end
+ end
+
+ describe 'POST /links' do
+ context 'when unauthenticated' do
+ it 'returns 401' do
+ target_issue = create(:issue)
+
+ post api("/projects/#{project.id}/issues/#{issue.iid}/links"),
+ params: { target_project_id: target_issue.project.id, target_issue_iid: target_issue.iid }
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when authenticated' do
+ context 'given target project not found' do
+ it 'returns 404' do
+ target_issue = create(:issue)
+
+ post api("/projects/#{project.id}/issues/#{issue.iid}/links", user),
+ params: { target_project_id: -1, target_issue_iid: target_issue.iid }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+ end
+
+ context 'given target issue not found' do
+ it 'returns 404' do
+ target_project = create(:project, :public)
+
+ post api("/projects/#{project.id}/issues/#{issue.iid}/links", user),
+ params: { target_project_id: target_project.id, target_issue_iid: non_existing_record_iid }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Not found')
+ end
+ end
+
+ context 'when user does not have write access to given issue' do
+ it 'returns 404' do
+ unauthorized_project = create(:project)
+ target_issue = create(:issue, project: unauthorized_project)
+ unauthorized_project.add_guest(user)
+
+ post api("/projects/#{project.id}/issues/#{issue.iid}/links", user),
+ params: { target_project_id: unauthorized_project.id, target_issue_iid: target_issue.iid }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('No Issue found for given params')
+ end
+ end
+
+ context 'when trying to relate to a confidential issue' do
+ it 'returns 404' do
+ project = create(:project, :public)
+ target_issue = create(:issue, :confidential, project: project)
+
+ post api("/projects/#{project.id}/issues/#{issue.iid}/links", user),
+ params: { target_project_id: project.id, target_issue_iid: target_issue.iid }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Not found')
+ end
+ end
+
+ context 'when trying to relate to a private project issue' do
+ it 'returns 404' do
+ project = create(:project, :private)
+ target_issue = create(:issue, project: project)
+
+ post api("/projects/#{project.id}/issues/#{issue.iid}/links", user),
+ params: { target_project_id: project.id, target_issue_iid: target_issue.iid }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+ end
+
+ context 'when user has ability to create an issue link' do
+ let_it_be(:target_issue) { create(:issue, project: project) }
+
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'returns 201 status and contains the expected link response' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/links", user),
+ params: { target_project_id: project.id, target_issue_iid: target_issue.iid, link_type: 'relates_to' }
+
+ expect_link_response(link_type: 'relates_to')
+ end
+
+ it 'returns 201 when sending full path of target project' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/links", user),
+ params: { target_project_id: project.full_path, target_issue_iid: target_issue.iid }
+
+ expect_link_response
+ end
+
+ def expect_link_response(link_type: 'relates_to')
+ expect(response).to have_gitlab_http_status(:created)
+ expect(response).to match_response_schema('public_api/v4/issue_link')
+ expect(json_response['link_type']).to eq(link_type)
+ end
+ end
+ end
+ end
+
+ describe 'DELETE /links/:issue_link_id' do
+ context 'when unauthenticated' do
+ it 'returns 401' do
+ issue_link = create(:issue_link)
+
+ delete api("/projects/#{project.id}/issues/#{issue.iid}/links/#{issue_link.id}")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when authenticated' do
+ context 'when user does not have write access to given issue link' do
+ it 'returns 404' do
+ unauthorized_project = create(:project)
+ target_issue = create(:issue, project: unauthorized_project)
+ issue_link = create(:issue_link, source: issue, target: target_issue)
+ unauthorized_project.add_guest(user)
+
+ delete api("/projects/#{project.id}/issues/#{issue.iid}/links/#{issue_link.id}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('No Issue Link found')
+ end
+ end
+
+ context 'issue link not found' do
+ it 'returns 404' do
+ delete api("/projects/#{project.id}/issues/#{issue.iid}/links/#{non_existing_record_id}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Not found')
+ end
+ end
+
+ context 'when trying to delete a link with a private project issue' do
+ it 'returns 404' do
+ project = create(:project, :private)
+ target_issue = create(:issue, project: project)
+ issue_link = create(:issue_link, source: issue, target: target_issue)
+
+ delete api("/projects/#{project.id}/issues/#{issue.iid}/links/#{issue_link.id}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+ end
+
+ context 'when user has ability to delete the issue link' do
+ it 'returns 200' do
+ target_issue = create(:issue, project: project)
+ issue_link = create(:issue_link, source: issue, target: target_issue)
+ project.add_reporter(user)
+
+ delete api("/projects/#{project.id}/issues/#{issue.iid}/links/#{issue_link.id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/issue_link')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb
index b0fbf3bf66d..3870c78deee 100644
--- a/spec/requests/api/issues/get_group_issues_spec.rb
+++ b/spec/requests/api/issues/get_group_issues_spec.rb
@@ -402,30 +402,76 @@ RSpec.describe API::Issues do
expect_paginated_array_response([group_closed_issue.id, group_issue.id])
end
- it 'returns an array of labeled group issues' do
- get api(base_url, user), params: { labels: group_label.title }
+ shared_examples 'labels parameter' do
+ it 'returns an array of labeled group issues' do
+ get api(base_url, user), params: { labels: group_label.title }
- expect_paginated_array_response(group_issue.id)
- expect(json_response.first['labels']).to eq([group_label.title])
- end
+ expect_paginated_array_response(group_issue.id)
+ expect(json_response.first['labels']).to eq([group_label.title])
+ end
- it 'returns an array of labeled group issues with labels param as array' do
- get api(base_url, user), params: { labels: [group_label.title] }
+ it 'returns an array of labeled group issues' do
+ get api(base_url, user), params: { labels: group_label.title }
- expect_paginated_array_response(group_issue.id)
- expect(json_response.first['labels']).to eq([group_label.title])
+ expect_paginated_array_response(group_issue.id)
+ expect(json_response.first['labels']).to eq([group_label.title])
+ end
+
+ it 'returns an array of labeled group issues with labels param as array' do
+ get api(base_url, user), params: { labels: [group_label.title] }
+
+ expect_paginated_array_response(group_issue.id)
+ expect(json_response.first['labels']).to eq([group_label.title])
+ end
+
+ it 'returns an array of labeled group issues where all labels match' do
+ get api(base_url, user), params: { labels: "#{group_label.title},foo,bar" }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of labeled group issues where all labels match with labels param as array' do
+ get api(base_url, user), params: { labels: [group_label.title, 'foo', 'bar'] }
+
+ expect_paginated_array_response([])
+ end
+
+ context 'with labeled issues' do
+ let(:group_issue2) { create :issue, project: group_project }
+ let(:label_b) { create(:label, title: 'foo', project: group_project) }
+ let(:label_c) { create(:label, title: 'bar', project: group_project) }
+
+ before do
+ create(:label_link, label: group_label, target: group_issue2)
+ create(:label_link, label: label_b, target: group_issue)
+ create(:label_link, label: label_b, target: group_issue2)
+ create(:label_link, label: label_c, target: group_issue)
+
+ get api(base_url, user), params: params
+ end
+
+ let(:issue) { group_issue }
+ let(:issue2) { group_issue2 }
+ let(:label) { group_label }
+
+ it_behaves_like 'labeled issues with labels and label_name params'
+ end
end
- it 'returns an array of labeled group issues where all labels match' do
- get api(base_url, user), params: { labels: "#{group_label.title},foo,bar" }
+ context 'when `optimized_issuable_label_filter` feature flag is off' do
+ before do
+ stub_feature_flags(optimized_issuable_label_filter: false)
+ end
- expect_paginated_array_response([])
+ it_behaves_like 'labels parameter'
end
- it 'returns an array of labeled group issues where all labels match with labels param as array' do
- get api(base_url, user), params: { labels: [group_label.title, 'foo', 'bar'] }
+ context 'when `optimized_issuable_label_filter` feature flag is on' do
+ before do
+ stub_feature_flags(optimized_issuable_label_filter: true)
+ end
- expect_paginated_array_response([])
+ it_behaves_like 'labels parameter'
end
it 'returns issues matching given search string for title' do
@@ -440,27 +486,6 @@ RSpec.describe API::Issues do
expect_paginated_array_response(group_issue.id)
end
- context 'with labeled issues' do
- let(:group_issue2) { create :issue, project: group_project }
- let(:label_b) { create(:label, title: 'foo', project: group_project) }
- let(:label_c) { create(:label, title: 'bar', project: group_project) }
-
- before do
- create(:label_link, label: group_label, target: group_issue2)
- create(:label_link, label: label_b, target: group_issue)
- create(:label_link, label: label_b, target: group_issue2)
- create(:label_link, label: label_c, target: group_issue)
-
- get api(base_url, user), params: params
- end
-
- let(:issue) { group_issue }
- let(:issue2) { group_issue2 }
- let(:label) { group_label }
-
- it_behaves_like 'labeled issues with labels and label_name params'
- end
-
context 'with archived projects' do
let_it_be(:archived_issue) do
create(
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index b638a65d65e..b8cbddd9ed4 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -87,6 +87,46 @@ RSpec.describe API::Issues do
end
end
+ describe 'GET /issues/:id' do
+ context 'when unauthorized' do
+ it 'returns unauthorized' do
+ get api("/issues/#{issue.id}" )
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when authorized' do
+ context 'as a normal user' do
+ it 'returns forbidden' do
+ get api("/issues/#{issue.id}", user )
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'as an admin' do
+ context 'when issue exists' do
+ it 'returns the issue' do
+ get api("/issues/#{issue.id}", admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.dig('author', 'id')).to eq(issue.author.id)
+ expect(json_response['description']).to eq(issue.description)
+ end
+ end
+
+ context 'when issue does not exist' do
+ it 'returns 404' do
+ get api("/issues/0", admin)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+ end
+
describe 'GET /issues' do
context 'when unauthenticated' do
it 'returns an array of all issues' do
@@ -128,6 +168,11 @@ RSpec.describe API::Issues do
expect_paginated_array_response([issue.id, closed_issue.id])
end
+ it 'responds with a 401 instead of the specified issue' do
+ get api("/issues/#{issue.id}")
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
context 'issues_statistics' do
it 'returns authentication error without any scope' do
get api('/issues_statistics')
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 77d5a4f26a8..2d57146fbc9 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -5,32 +5,6 @@ require 'spec_helper'
RSpec.describe API::Jobs do
include HttpIOHelpers
- shared_examples 'a job with artifacts and trace' do |result_is_array: true|
- context 'with artifacts and trace' do
- let!(:second_job) { create(:ci_build, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline) }
-
- it 'returns artifacts and trace data', :skip_before_request do
- get api(api_endpoint, api_user)
- json_job = result_is_array ? json_response.select { |job| job['id'] == second_job.id }.first : json_response
-
- expect(json_job['artifacts_file']).not_to be_nil
- expect(json_job['artifacts_file']).not_to be_empty
- expect(json_job['artifacts_file']['filename']).to eq(second_job.artifacts_file.filename)
- expect(json_job['artifacts_file']['size']).to eq(second_job.artifacts_file.size)
- expect(json_job['artifacts']).not_to be_nil
- expect(json_job['artifacts']).to be_an Array
- expect(json_job['artifacts'].size).to eq(second_job.job_artifacts.length)
- json_job['artifacts'].each do |artifact|
- expect(artifact).not_to be_nil
- file_type = Ci::JobArtifact.file_types[artifact['file_type']]
- expect(artifact['size']).to eq(second_job.job_artifacts.find_by(file_type: file_type).size)
- expect(artifact['filename']).to eq(second_job.job_artifacts.find_by(file_type: file_type).filename)
- expect(artifact['file_format']).to eq(second_job.job_artifacts.find_by(file_type: file_type).file_format)
- end
- end
- end
- end
-
let_it_be(:project, reload: true) do
create(:project, :repository, public_builds: false)
end
@@ -56,7 +30,7 @@ RSpec.describe API::Jobs do
end
describe 'GET /projects/:id/jobs' do
- let(:query) { Hash.new }
+ let(:query) { {} }
before do |example|
unless example.metadata[:skip_before_request]
@@ -166,295 +140,6 @@ RSpec.describe API::Jobs do
end
end
- describe 'GET /projects/:id/pipelines/:pipeline_id/jobs' do
- let(:query) { Hash.new }
-
- before do |example|
- unless example.metadata[:skip_before_request]
- job
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), params: query
- end
- end
-
- context 'authorized user' do
- it 'returns pipeline jobs' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- end
-
- it 'returns correct values' do
- expect(json_response).not_to be_empty
- expect(json_response.first['commit']['id']).to eq project.commit.id
- expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
- expect(json_response.first['artifacts_file']).to be_nil
- expect(json_response.first['artifacts']).to be_an Array
- expect(json_response.first['artifacts']).to be_empty
- end
-
- it_behaves_like 'a job with artifacts and trace' do
- let(:api_endpoint) { "/projects/#{project.id}/pipelines/#{pipeline.id}/jobs" }
- end
-
- it 'returns pipeline data' do
- json_job = json_response.first
-
- expect(json_job['pipeline']).not_to be_empty
- expect(json_job['pipeline']['id']).to eq job.pipeline.id
- expect(json_job['pipeline']['ref']).to eq job.pipeline.ref
- expect(json_job['pipeline']['sha']).to eq job.pipeline.sha
- expect(json_job['pipeline']['status']).to eq job.pipeline.status
- end
-
- context 'filter jobs with one scope element' do
- let(:query) { { 'scope' => 'pending' } }
-
- it do
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an Array
- end
- end
-
- context 'filter jobs with array of scope elements' do
- let(:query) { { scope: %w(pending running) } }
-
- it do
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an Array
- end
- end
-
- context 'respond 400 when scope contains invalid state' do
- let(:query) { { scope: %w(unknown running) } }
-
- it { expect(response).to have_gitlab_http_status(:bad_request) }
- end
-
- context 'jobs in different pipelines' do
- let!(:pipeline2) { create(:ci_empty_pipeline, project: project) }
- let!(:job2) { create(:ci_build, pipeline: pipeline2) }
-
- it 'excludes jobs from other pipelines' do
- json_response.each { |job| expect(job['pipeline']['id']).to eq(pipeline.id) }
- end
- end
-
- context 'when config source not ci' do
- let(:non_ci_config_source) { ::Ci::PipelineEnums.non_ci_config_source_values.first }
- let(:pipeline) do
- create(:ci_pipeline, config_source: non_ci_config_source, project: project)
- end
-
- it 'returns the specified pipeline' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response[0]['pipeline']['sha']).to eq(pipeline.sha.to_s)
- end
- end
-
- it 'avoids N+1 queries' do
- control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), params: query
- end.count
-
- create_list(:ci_build, 3, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline)
-
- expect do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), params: query
- end.not_to exceed_all_query_limit(control_count)
- end
- end
-
- context 'unauthorized user' do
- context 'when user is not logged in' do
- let(:api_user) { nil }
-
- it 'does not return jobs' do
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- context 'when user is guest' do
- let(:api_user) { guest }
-
- it 'does not return jobs' do
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
- end
- end
-
- describe 'GET /projects/:id/pipelines/:pipeline_id/bridges' do
- let!(:bridge) { create(:ci_bridge, pipeline: pipeline) }
- let(:downstream_pipeline) { create(:ci_pipeline) }
-
- let!(:pipeline_source) do
- create(:ci_sources_pipeline,
- source_pipeline: pipeline,
- source_project: project,
- source_job: bridge,
- pipeline: downstream_pipeline,
- project: downstream_pipeline.project)
- end
-
- let(:query) { Hash.new }
-
- before do |example|
- unless example.metadata[:skip_before_request]
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
- end
- end
-
- context 'authorized user' do
- it 'returns pipeline bridges' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- end
-
- it 'returns correct values' do
- expect(json_response).not_to be_empty
- expect(json_response.first['commit']['id']).to eq project.commit.id
- expect(json_response.first['id']).to eq bridge.id
- expect(json_response.first['name']).to eq bridge.name
- expect(json_response.first['stage']).to eq bridge.stage
- end
-
- it 'returns pipeline data' do
- json_bridge = json_response.first
-
- expect(json_bridge['pipeline']).not_to be_empty
- expect(json_bridge['pipeline']['id']).to eq bridge.pipeline.id
- expect(json_bridge['pipeline']['ref']).to eq bridge.pipeline.ref
- expect(json_bridge['pipeline']['sha']).to eq bridge.pipeline.sha
- expect(json_bridge['pipeline']['status']).to eq bridge.pipeline.status
- end
-
- it 'returns downstream pipeline data' do
- json_bridge = json_response.first
-
- expect(json_bridge['downstream_pipeline']).not_to be_empty
- expect(json_bridge['downstream_pipeline']['id']).to eq downstream_pipeline.id
- expect(json_bridge['downstream_pipeline']['ref']).to eq downstream_pipeline.ref
- expect(json_bridge['downstream_pipeline']['sha']).to eq downstream_pipeline.sha
- expect(json_bridge['downstream_pipeline']['status']).to eq downstream_pipeline.status
- end
-
- context 'filter bridges' do
- before do
- create_bridge(pipeline, :pending)
- create_bridge(pipeline, :running)
- end
-
- context 'with one scope element' do
- let(:query) { { 'scope' => 'pending' } }
-
- it :skip_before_request do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an Array
- expect(json_response.count).to eq 1
- expect(json_response.first["status"]).to eq "pending"
- end
- end
-
- context 'with array of scope elements' do
- let(:query) { { scope: %w(pending running) } }
-
- it :skip_before_request do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an Array
- expect(json_response.count).to eq 2
- json_response.each { |r| expect(%w(pending running).include?(r['status'])).to be true }
- end
- end
- end
-
- context 'respond 400 when scope contains invalid state' do
- let(:query) { { scope: %w(unknown running) } }
-
- it { expect(response).to have_gitlab_http_status(:bad_request) }
- end
-
- context 'bridges in different pipelines' do
- let!(:pipeline2) { create(:ci_empty_pipeline, project: project) }
- let!(:bridge2) { create(:ci_bridge, pipeline: pipeline2) }
-
- it 'excludes bridges from other pipelines' do
- json_response.each { |bridge| expect(bridge['pipeline']['id']).to eq(pipeline.id) }
- end
- end
-
- it 'avoids N+1 queries' do
- control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
- end.count
-
- 3.times { create_bridge(pipeline) }
-
- expect do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
- end.not_to exceed_all_query_limit(control_count)
- end
- end
-
- context 'unauthorized user' do
- context 'when user is not logged in' do
- let(:api_user) { nil }
-
- it 'does not return bridges' do
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- context 'when user is guest' do
- let(:api_user) { guest }
-
- it 'does not return bridges' do
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
- context 'when user has no read access for pipeline' do
- before do
- allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?).with(api_user, :read_pipeline, pipeline).and_return(false)
- end
-
- it 'does not return bridges' do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user)
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
- context 'when user has no read_build access for project' do
- before do
- allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?).with(api_user, :read_build, project).and_return(false)
- end
-
- it 'does not return bridges' do
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user)
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
- end
-
- def create_bridge(pipeline, status = :created)
- create(:ci_bridge, status: status, pipeline: pipeline).tap do |bridge|
- downstream_pipeline = create(:ci_pipeline)
- create(:ci_sources_pipeline,
- source_pipeline: pipeline,
- source_project: pipeline.project,
- source_job: bridge,
- pipeline: downstream_pipeline,
- project: downstream_pipeline.project)
- end
- end
- end
-
describe 'GET /projects/:id/jobs/:job_id' do
before do |example|
unless example.metadata[:skip_before_request]
diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb
index b74887762a2..0a23aed109b 100644
--- a/spec/requests/api/maven_packages_spec.rb
+++ b/spec/requests/api/maven_packages_spec.rb
@@ -283,7 +283,7 @@ RSpec.describe API::MavenPackages do
context 'internal project' do
before do
- group.group_member(user).destroy
+ group.group_member(user).destroy!
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
@@ -542,6 +542,18 @@ RSpec.describe API::MavenPackages do
context 'when params from workhorse are correct' do
let(:params) { { file: file_upload } }
+ context 'file size is too large' do
+ it 'rejects the request' do
+ allow_next_instance_of(UploadedFile) do |uploaded_file|
+ allow(uploaded_file).to receive(:size).and_return(project.actual_limits.maven_max_file_size + 1)
+ end
+
+ upload_file_with_token(params)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
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
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 23889912d7a..de52087340c 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -258,8 +258,8 @@ RSpec.describe API::Members do
it 'does not create the member if group level is higher' do
parent = create(:group)
- group.update(parent: parent)
- project.update(group: group)
+ group.update!(parent: parent)
+ project.update!(group: group)
parent.add_developer(stranger)
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
@@ -272,8 +272,8 @@ RSpec.describe API::Members do
it 'creates the member if group level is lower' do
parent = create(:group)
- group.update(parent: parent)
- project.update(group: group)
+ group.update!(parent: parent)
+ project.update!(group: group)
parent.add_developer(stranger)
post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
index 3f41a7a034d..2e6cbe7bee7 100644
--- a/spec/requests/api/merge_request_diffs_spec.rb
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -8,8 +8,8 @@ RSpec.describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
let!(:project) { merge_request.target_project }
before do
- merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
- merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ merge_request.merge_request_diffs.create!(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ merge_request.merge_request_diffs.create!(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
project.add_maintainer(user)
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index d4c05b4b198..2757c56e0fe 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -5,23 +5,19 @@ require "spec_helper"
RSpec.describe API::MergeRequests do
include ProjectForksHelper
- let(:base_time) { Time.now }
+ let_it_be(:base_time) { Time.now }
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:admin) { create(:user, :admin) }
- let(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
- let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
- let(:milestone1) { create(:milestone, title: '0.9', project: project) }
- let(:merge_request_context_commit) {create(:merge_request_context_commit, message: 'test')}
- let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignees: [user], merge_request_context_commits: [merge_request_context_commit], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) }
- let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
- let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignees: [user], source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
- let!(:merge_request_locked) { create(:merge_request, state: "locked", milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Locked test", created_at: base_time + 1.second) }
- let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
- let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
+ let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
+
+ let(:milestone1) { create(:milestone, title: '0.9', project: project) }
+ let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
let(:label) { create(:label, title: 'label', color: '#FFAABB', project: project) }
let(:label2) { create(:label, title: 'a-test', color: '#FFFFFF', project: project) }
+ let(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) }
+
before do
project.add_reporter(user)
project.add_reporter(user2)
@@ -29,6 +25,16 @@ RSpec.describe API::MergeRequests do
stub_licensed_features(multiple_merge_request_assignees: false)
end
+ shared_context 'with merge requests' do
+ let_it_be(:milestone1) { create(:milestone, title: '0.9', project: project) }
+ let_it_be(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) }
+ let_it_be(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
+ let_it_be(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignees: [user], source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
+ let_it_be(:merge_request_locked) { create(:merge_request, state: "locked", milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Locked test", created_at: base_time + 1.second) }
+ let_it_be(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
+ let_it_be(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
+ end
+
shared_context 'with labels' do
before do
create(:label_link, label: label, target: merge_request)
@@ -68,6 +74,7 @@ RSpec.describe API::MergeRequests do
context 'when merge request is unchecked' do
let(:check_service_class) { MergeRequests::MergeabilityCheckService }
let(:mr_entity) { json_response.find { |mr| mr['id'] == merge_request.id } }
+ let(:merge_request) { create(:merge_request, :simple, author: user, source_project: project, title: "Test") }
before do
merge_request.mark_as_unchecked!
@@ -426,14 +433,13 @@ RSpec.describe API::MergeRequests do
end
context 'NOT params' do
- let(:merge_request2) 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',
@@ -442,6 +448,8 @@ RSpec.describe API::MergeRequests do
)
end
+ let!(:merge_request_context_commit) { create(:merge_request_context_commit, merge_request: merge_request2, message: 'test') }
+
before do
create(:label_link, label: label, target: merge_request)
create(:label_link, label: label2, target: merge_request2)
@@ -527,6 +535,8 @@ RSpec.describe API::MergeRequests do
end
describe 'GET /merge_requests' do
+ include_context 'with merge requests'
+
context 'when unauthenticated' do
it 'returns an array of all merge requests' do
get api('/merge_requests', user), params: { scope: 'all' }
@@ -563,9 +573,9 @@ RSpec.describe API::MergeRequests do
end
context 'when authenticated' do
- let!(:project2) { create(:project, :public, namespace: user.namespace) }
- let!(:merge_request2) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project2, target_project: project2) }
- let(:user2) { create(:user) }
+ let_it_be(:project2) { create(:project, :public, :repository, namespace: user.namespace) }
+ let_it_be(:merge_request2) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project2, target_project: project2) }
+ let_it_be(:user2) { create(:user) }
it 'returns an array of all merge requests except unauthorized ones' do
get api('/merge_requests', user), params: { scope: :all }
@@ -778,8 +788,8 @@ RSpec.describe API::MergeRequests do
end
context 'search params' do
- before do
- merge_request.update(title: 'Search title', description: 'Search description')
+ let_it_be(:merge_request) do
+ create(:merge_request, :simple, author: user, source_project: project, target_project: project, title: 'Search title', description: 'Search description')
end
it 'returns merge requests matching given search string for title' do
@@ -818,6 +828,8 @@ RSpec.describe API::MergeRequests do
end
describe "GET /projects/:id/merge_requests" do
+ include_context 'with merge requests'
+
let(:endpoint_path) { "/projects/#{project.id}/merge_requests" }
it_behaves_like 'merge requests list'
@@ -845,7 +857,9 @@ RSpec.describe API::MergeRequests do
end
context 'a project which enforces all discussions to be resolved' do
- let!(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) }
+ let_it_be(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) }
+
+ include_context 'with merge requests'
it 'avoids N+1 queries' do
control = ActiveRecord::QueryRecorder.new do
@@ -864,6 +878,9 @@ RSpec.describe API::MergeRequests do
describe "GET /groups/:id/merge_requests" do
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: group, only_allow_merge_if_pipeline_succeeds: false) }
+
+ include_context 'with merge requests'
+
let(:endpoint_path) { "/groups/#{group.id}/merge_requests" }
before do
@@ -877,6 +894,8 @@ RSpec.describe API::MergeRequests do
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: subgroup, only_allow_merge_if_pipeline_succeeds: false) }
+ include_context 'with merge requests'
+
it_behaves_like 'merge requests list'
end
@@ -893,7 +912,7 @@ RSpec.describe API::MergeRequests do
let(:parent_group) { create(:group) }
before do
- group.update(parent_id: parent_group.id)
+ group.update!(parent_id: parent_group.id)
merge_request_merged.reload
end
@@ -936,6 +955,8 @@ RSpec.describe API::MergeRequests do
end
describe "GET /projects/:id/merge_requests/:merge_request_iid" do
+ let(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], milestone: milestone, source_project: project, source_branch: 'markdown', title: "Test") }
+
it 'matches json schema' do
merge_request = create(:merge_request, :with_test_reports, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Test", created_at: base_time)
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
@@ -1006,7 +1027,7 @@ RSpec.describe API::MergeRequests do
let(:non_member) { create(:user) }
before do
- merge_request.update(author: non_member)
+ merge_request.update!(author: non_member)
end
it 'exposes first_contribution as true' do
@@ -1059,9 +1080,12 @@ RSpec.describe API::MergeRequests do
end
context 'head_pipeline' do
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, :simple, author: user, source_project: project, source_branch: 'markdown', title: "Test") }
+
before do
- merge_request.update(head_pipeline: create(:ci_pipeline))
- merge_request.project.project_feature.update(builds_access_level: 10)
+ merge_request.update!(head_pipeline: create(:ci_pipeline))
+ merge_request.project.project_feature.update!(builds_access_level: 10)
end
context 'when user can read the pipeline' do
@@ -1188,11 +1212,13 @@ RSpec.describe API::MergeRequests do
describe 'GET /projects/:id/merge_requests/:merge_request_iid/participants' do
it_behaves_like 'issuable participants endpoint' do
- let(:entity) { merge_request }
+ let(:entity) { create(:merge_request, :simple, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) }
end
end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/commits' do
+ include_context 'with merge requests'
+
it 'returns a 200 when merge request is valid' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits", user)
commit = merge_request.commits.first
@@ -1216,6 +1242,9 @@ RSpec.describe API::MergeRequests do
end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/:context_commits' do
+ let_it_be(:merge_request) { create(:merge_request, :simple, author: user, source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) }
+ let_it_be(:merge_request_context_commit) { create(:merge_request_context_commit, merge_request: merge_request, message: 'test') }
+
it 'returns a 200 when merge request is valid' do
context_commit = merge_request.context_commits.first
@@ -1234,6 +1263,8 @@ RSpec.describe API::MergeRequests do
end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/changes' do
+ let_it_be(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) }
+
it 'returns the change information of the merge_request' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user)
@@ -1254,6 +1285,8 @@ RSpec.describe API::MergeRequests do
end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/pipelines' do
+ let_it_be(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) }
+
context 'when authorized' do
let!(:pipeline) { create(:ci_empty_pipeline, project: project, user: user, ref: merge_request.source_branch, sha: merge_request.diff_head_sha) }
let!(:pipeline2) { create(:ci_empty_pipeline, project: project) }
@@ -1308,16 +1341,15 @@ RSpec.describe API::MergeRequests do
})
end
- let(:project) do
+ let_it_be(:project) do
create(:project, :private, :repository,
creator: user,
namespace: user.namespace,
only_allow_merge_if_pipeline_succeeds: false)
end
- let(:merge_request) do
+ let_it_be(:merge_request) do
create(:merge_request, :with_detached_merge_request_pipeline,
- milestone: milestone1,
author: user,
assignees: [user],
source_project: project,
@@ -1351,7 +1383,7 @@ RSpec.describe API::MergeRequests do
end
context 'when the merge request does not exist' do
- let(:merge_request_iid) { 777 }
+ let(:merge_request_iid) { non_existing_record_id }
it 'responds with a blank 404' do
expect { request }.not_to change(Ci::Pipeline, :count)
@@ -1604,7 +1636,7 @@ RSpec.describe API::MergeRequests do
end.to change { MergeRequest.count }.by(0)
expect(response).to have_gitlab_http_status(:conflict)
- expect(json_response['message']).to eq(["Another open merge request already exists for this source branch: !5"])
+ expect(json_response['message']).to eq(["Another open merge request already exists for this source branch: !1"])
end
end
@@ -1659,7 +1691,7 @@ RSpec.describe API::MergeRequests do
end
it 'returns 403 when target project has disabled merge requests' do
- project.project_feature.update(merge_requests_access_level: 0)
+ project.project_feature.update!(merge_requests_access_level: 0)
post api("/projects/#{forked_project.id}/merge_requests", user2),
params: {
@@ -1778,6 +1810,7 @@ RSpec.describe API::MergeRequests do
before do
create(:merge_request_context_commit, merge_request: merge_request, sha: commit.id)
end
+
it 'returns 400 when the context commit is already created' do
post api("/projects/#{project.id}/merge_requests/#{merge_request_iid}/context_commits", authenticated_user), params: params
expect(response).to have_gitlab_http_status(:bad_request)
@@ -1937,7 +1970,9 @@ RSpec.describe API::MergeRequests do
end
describe "PUT /projects/:id/merge_requests/:merge_request_iid/merge", :clean_gitlab_redis_cache do
- let(:pipeline) { create(:ci_pipeline) }
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+ let(:merge_request) { create(:merge_request, :simple, author: user, source_project: project, source_branch: 'markdown', title: 'Test') }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
it "returns merge_request in case of success" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
@@ -2111,7 +2146,7 @@ RSpec.describe API::MergeRequests do
let(:source_branch) { merge_request.source_branch }
before do
- merge_request.update(merge_params: { 'force_remove_source_branch' => true })
+ merge_request.update!(merge_params: { 'force_remove_source_branch' => true })
end
it 'removes the source branch' do
@@ -2138,7 +2173,7 @@ RSpec.describe API::MergeRequests do
let(:merge_request) { create(:merge_request, :rebased, source_project: project, squash: true) }
before do
- project.update(merge_requests_ff_only_enabled: true)
+ project.update!(merge_requests_ff_only_enabled: true)
end
it "records the squash commit SHA and returns it in the response" do
@@ -2169,9 +2204,7 @@ RSpec.describe API::MergeRequests do
end
context 'when merge-ref is not synced with merge status' do
- before do
- merge_request.update!(merge_status: 'cannot_be_merged')
- end
+ let(:merge_request) { create(:merge_request, :simple, author: user, source_project: project, source_branch: 'markdown', merge_status: 'cannot_be_merged') }
it 'returns 200 if MR can be merged' do
get api(url, user)
@@ -2230,7 +2263,7 @@ RSpec.describe API::MergeRequests do
describe "PUT /projects/:id/merge_requests/:merge_request_iid" do
context 'updates force_remove_source_branch properly' do
it 'sets to false' do
- merge_request.update(merge_params: { 'force_remove_source_branch' => true } )
+ merge_request.update!(merge_params: { 'force_remove_source_branch' => true } )
expect(merge_request.force_remove_source_branch?).to be_truthy
@@ -2242,7 +2275,7 @@ RSpec.describe API::MergeRequests do
end
it 'sets to true' do
- merge_request.update(merge_params: { 'force_remove_source_branch' => false } )
+ merge_request.update!(merge_params: { 'force_remove_source_branch' => false } )
expect(merge_request.force_remove_source_branch?).to be_falsey
@@ -2254,6 +2287,7 @@ RSpec.describe API::MergeRequests do
end
context 'with a merge request across forks' do
+ let(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
let(:fork_owner) { create(:user) }
let(:source_project) { fork_project(project, fork_owner) }
let(:target_project) { project }
@@ -2320,12 +2354,46 @@ RSpec.describe API::MergeRequests do
expect(json_response['squash']).to be_truthy
end
- it "returns merge_request with renamed target_branch" do
+ it "updates target_branch and returns merge_request" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: { target_branch: "wiki" }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['target_branch']).to eq('wiki')
end
+ context "forked projects" do
+ let_it_be(:user2) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+ let!(:forked_project) { fork_project(project, user2, repository: true) }
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: forked_project,
+ target_project: project,
+ source_branch: "fixes")
+ end
+
+ shared_examples "update of allow_collaboration and allow_maintainer_to_push" do |request_value, expected_value|
+ %w[allow_collaboration allow_maintainer_to_push].each do |attr|
+ it "attempts to update #{attr} to #{request_value} and returns #{expected_value} for `allow_collaboration`" do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user2), params: { attr => request_value }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response["allow_collaboration"]).to eq(expected_value)
+ expect(json_response["allow_maintainer_to_push"]).to eq(expected_value)
+ end
+ end
+ end
+
+ context "when source project is public (i.e. MergeRequest#collaborative_push_possible? == true)" do
+ it_behaves_like "update of allow_collaboration and allow_maintainer_to_push", true, true
+ end
+
+ context "when source project is private (i.e. MergeRequest#collaborative_push_possible? == false)" do
+ let(:project) { create(:project, :private, :repository) }
+
+ it_behaves_like "update of allow_collaboration and allow_maintainer_to_push", true, false
+ end
+ end
+
it "returns merge_request that removes the source branch" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: { remove_source_branch: true }
@@ -2717,7 +2785,7 @@ RSpec.describe API::MergeRequests do
end
describe 'Time tracking' do
- let(:issuable) { merge_request }
+ let!(:issuable) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) }
include_examples 'time tracking endpoints', 'merge_request'
end
@@ -2726,7 +2794,7 @@ RSpec.describe API::MergeRequests do
merge_request
merge_request.created_at += 1.hour
merge_request.updated_at += 30.minutes
- merge_request.save
+ merge_request.save!
merge_request
end
@@ -2734,7 +2802,7 @@ RSpec.describe API::MergeRequests do
merge_request_closed
merge_request_closed.created_at -= 1.hour
merge_request_closed.updated_at -= 30.minutes
- merge_request_closed.save
+ merge_request_closed.save!
merge_request_closed
end
end
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index ca4ebd3689f..baab72d106f 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe API::Notes do
context "issue is confidential" do
before do
- ext_issue.update(confidential: true)
+ ext_issue.update!(confidential: true)
end
it "returns 404" do
@@ -183,7 +183,7 @@ RSpec.describe API::Notes do
context "when issue is confidential" do
before do
- issue.update(confidential: true)
+ issue.update!(confidential: true)
end
it "returns 404" do
diff --git a/spec/requests/api/nuget_packages_spec.rb b/spec/requests/api/nuget_packages_spec.rb
index 87c62ec41c6..62f244c433b 100644
--- a/spec/requests/api/nuget_packages_spec.rb
+++ b/spec/requests/api/nuget_packages_spec.rb
@@ -220,6 +220,18 @@ RSpec.describe API::NugetPackages do
it_behaves_like 'rejects nuget access with unknown project id'
it_behaves_like 'rejects nuget access with invalid project id'
+
+ context 'file size above maximum limit' do
+ let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) }
+
+ before do
+ allow_next_instance_of(UploadedFile) do |uploaded_file|
+ allow(uploaded_file).to receive(:size).and_return(project.actual_limits.nuget_max_file_size + 1)
+ end
+ end
+
+ it_behaves_like 'returning response status', :bad_request
+ end
end
end
diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb
index f5971054b3c..23d5df873d4 100644
--- a/spec/requests/api/oauth_tokens_spec.rb
+++ b/spec/requests/api/oauth_tokens_spec.rb
@@ -20,7 +20,7 @@ RSpec.describe 'OAuth tokens' do
request_oauth_token(user, client_basic_auth_header(client))
- expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('invalid_grant')
end
end
@@ -62,7 +62,7 @@ RSpec.describe 'OAuth tokens' do
request_oauth_token(user, basic_auth_header(client.uid, 'invalid secret'))
- expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('invalid_client')
end
end
@@ -72,7 +72,7 @@ RSpec.describe 'OAuth tokens' do
shared_examples 'does not create an access token' do
let(:user) { create(:user) }
- it { expect(response).to have_gitlab_http_status(:unauthorized) }
+ it { expect(response).to have_gitlab_http_status(:bad_request) }
end
context 'when user is blocked' do
diff --git a/spec/requests/api/pages/internal_access_spec.rb b/spec/requests/api/pages/internal_access_spec.rb
index c894a2d3ca4..4ac47f17b7e 100644
--- a/spec/requests/api/pages/internal_access_spec.rb
+++ b/spec/requests/api/pages/internal_access_spec.rb
@@ -72,7 +72,7 @@ RSpec.describe "Internal Project Pages Access" do
with_them do
before do
- project.project_feature.update(pages_access_level: pages_access_level)
+ project.project_feature.update!(pages_access_level: pages_access_level)
end
it "correct return value" do
if !with_user.nil?
diff --git a/spec/requests/api/pages/pages_spec.rb b/spec/requests/api/pages/pages_spec.rb
index 53e732928ff..f4c6de00e40 100644
--- a/spec/requests/api/pages/pages_spec.rb
+++ b/spec/requests/api/pages/pages_spec.rb
@@ -39,7 +39,9 @@ RSpec.describe API::Pages do
expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project).and_return true
expect(PagesWorker).to receive(:perform_in).with(5.minutes, :remove, project.namespace.full_path, anything)
- delete api("/projects/#{project.id}/pages", admin )
+ Sidekiq::Testing.inline! do
+ delete api("/projects/#{project.id}/pages", admin )
+ end
expect(project.reload.pages_metadatum.deployed?).to be(false)
end
diff --git a/spec/requests/api/pages/private_access_spec.rb b/spec/requests/api/pages/private_access_spec.rb
index ea5db691b14..c1c0e406508 100644
--- a/spec/requests/api/pages/private_access_spec.rb
+++ b/spec/requests/api/pages/private_access_spec.rb
@@ -72,7 +72,7 @@ RSpec.describe "Private Project Pages Access" do
with_them do
before do
- project.project_feature.update(pages_access_level: pages_access_level)
+ project.project_feature.update!(pages_access_level: pages_access_level)
end
it "correct return value" do
if !with_user.nil?
diff --git a/spec/requests/api/pages/public_access_spec.rb b/spec/requests/api/pages/public_access_spec.rb
index ae73cee91d5..c45b3a4c55e 100644
--- a/spec/requests/api/pages/public_access_spec.rb
+++ b/spec/requests/api/pages/public_access_spec.rb
@@ -72,7 +72,7 @@ RSpec.describe "Public Project Pages Access" do
with_them do
before do
- project.project_feature.update(pages_access_level: pages_access_level)
+ project.project_feature.update!(pages_access_level: pages_access_level)
end
it "correct return value" do
if !with_user.nil?
diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb
index d1e5df66b3f..71535e66353 100644
--- a/spec/requests/api/project_milestones_spec.rb
+++ b/spec/requests/api/project_milestones_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe API::ProjectMilestones do
let(:milestones) { [group_milestone, ancestor_group_milestone, milestone, closed_milestone] }
before_all do
- project.update(namespace: group)
+ project.update!(namespace: group)
end
it_behaves_like 'listing all milestones'
@@ -108,7 +108,7 @@ RSpec.describe API::ProjectMilestones do
let(:group) { create(:group) }
before do
- project.update(namespace: group)
+ project.update!(namespace: group)
end
context 'when user does not have permission to promote milestone' do
@@ -163,7 +163,7 @@ RSpec.describe API::ProjectMilestones do
context 'when project does not belong to group' do
before do
- project.update(namespace: user.namespace)
+ project.update!(namespace: user.namespace)
end
it 'returns 403' do
diff --git a/spec/requests/api/project_packages_spec.rb b/spec/requests/api/project_packages_spec.rb
index 0ece3bff8f9..2f0d0fc87ec 100644
--- a/spec/requests/api/project_packages_spec.rb
+++ b/spec/requests/api/project_packages_spec.rb
@@ -104,6 +104,8 @@ RSpec.describe API::ProjectPackages do
expect(json_response.first['name']).to include(package2.name)
end
end
+
+ it_behaves_like 'does not cause n^2 queries'
end
end
@@ -127,6 +129,22 @@ RSpec.describe API::ProjectPackages do
end
context 'without the need for a license' do
+ context 'with build info' do
+ it 'does not result in additional queries' do
+ control = ActiveRecord::QueryRecorder.new do
+ get api(package_url, user)
+ end
+
+ pipeline = create(:ci_pipeline, user: user)
+ create(:ci_build, user: user, pipeline: pipeline)
+ create(:package_build_info, package: package1, pipeline: pipeline)
+
+ expect do
+ get api(package_url, user)
+ end.not_to exceed_query_limit(control)
+ end
+ end
+
context 'project is public' do
it 'returns 200 and the package information' do
subject
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 9b876edae24..08c88873078 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -304,56 +304,10 @@ RSpec.describe API::ProjectSnippets do
let(:visibility_level) { Snippet::PUBLIC }
let(:snippet) { create(:project_snippet, :repository, author: admin, visibility_level: visibility_level, project: project) }
- it 'updates snippet' do
- new_content = 'New content'
- new_description = 'New description'
-
- update_snippet(params: { content: new_content, description: new_description, visibility: 'private' })
-
- expect(response).to have_gitlab_http_status(:ok)
- snippet.reload
- expect(snippet.content).to eq(new_content)
- expect(snippet.description).to eq(new_description)
- expect(snippet.visibility).to eq('private')
- end
-
- it 'updates snippet with content parameter' do
- new_content = 'New content'
- new_description = 'New description'
-
- update_snippet(params: { content: new_content, description: new_description })
-
- expect(response).to have_gitlab_http_status(:ok)
- snippet.reload
- expect(snippet.content).to eq(new_content)
- expect(snippet.description).to eq(new_description)
- end
-
- it 'returns 404 for invalid snippet id' do
- update_snippet(snippet_id: non_existing_record_id, params: { title: 'foo' })
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect(json_response['message']).to eq('404 Snippet Not Found')
- end
-
- it 'returns 400 for missing parameters' do
- update_snippet
-
- expect(response).to have_gitlab_http_status(:bad_request)
- end
-
- it 'returns 400 if content is blank' do
- update_snippet(params: { content: '' })
-
- expect(response).to have_gitlab_http_status(:bad_request)
- end
-
- it 'returns 400 if title is blank' do
- update_snippet(params: { title: '' })
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq 'title is empty'
- end
+ it_behaves_like 'snippet file updates'
+ it_behaves_like 'snippet non-file updates'
+ it_behaves_like 'snippet individual non-file updates'
+ it_behaves_like 'invalid snippet updates'
it_behaves_like 'update with repository actions' do
let(:snippet_without_repo) { create(:project_snippet, author: admin, project: project, visibility_level: visibility_level) }
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 46340f86f69..831b0d6e678 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -49,15 +49,15 @@ end
RSpec.describe API::Projects do
include ProjectForksHelper
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:user3) { create(:user) }
- let(:admin) { create(:admin) }
- let(:project) { create(:project, :repository, namespace: user.namespace) }
- let(:project2) { create(:project, namespace: user.namespace) }
- let(:project_member) { create(:project_member, :developer, user: user3, project: project) }
- let(:user4) { create(:user, username: 'user.with.dot') }
- let(:project3) do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:user3) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:project, reload: true) { create(:project, :repository, namespace: user.namespace) }
+ let_it_be(:project2, reload: true) { create(:project, namespace: user.namespace) }
+ let_it_be(:project_member) { create(:project_member, :developer, user: user3, project: project) }
+ let_it_be(:user4) { create(:user, username: 'user.with.dot') }
+ let_it_be(:project3, reload: true) do
create(:project,
:private,
:repository,
@@ -71,14 +71,14 @@ RSpec.describe API::Projects do
snippets_enabled: false)
end
- let(:project_member2) do
+ let_it_be(:project_member2) do
create(:project_member,
user: user4,
project: project3,
access_level: ProjectMember::MAINTAINER)
end
- let(:project4) do
+ let_it_be(:project4, reload: true) do
create(:project,
name: 'third_project',
path: 'third_project',
@@ -86,6 +86,8 @@ RSpec.describe API::Projects do
namespace: user4.namespace)
end
+ let(:user_projects) { [public_project, project, project2, project3] }
+
shared_context 'with language detection' do
let(:ruby) { create(:programming_language, name: 'Ruby') }
let(:javascript) { create(:programming_language, name: 'JavaScript') }
@@ -146,14 +148,7 @@ RSpec.describe API::Projects do
end
end
- let!(:public_project) { create(:project, :public, name: 'public_project') }
-
- before do
- project
- project2
- project3
- project4
- end
+ let_it_be(:public_project) { create(:project, :public, name: 'public_project') }
context 'when unauthenticated' do
it_behaves_like 'projects response' do
@@ -171,7 +166,7 @@ RSpec.describe API::Projects do
it_behaves_like 'projects response' do
let(:filter) { {} }
let(:current_user) { user }
- let(:projects) { [public_project, project, project2, project3] }
+ let(:projects) { user_projects }
end
it_behaves_like 'projects response without N + 1 queries' do
@@ -208,7 +203,7 @@ RSpec.describe API::Projects do
end
it 'does not include projects marked for deletion' do
- project.update(pending_delete: true)
+ project.update!(pending_delete: true)
get api('/projects', user)
@@ -257,7 +252,7 @@ RSpec.describe API::Projects do
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- statistics = json_response.first['statistics']
+ statistics = json_response.find { |p| p['id'] == project.id }['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
@@ -386,14 +381,14 @@ RSpec.describe API::Projects do
it_behaves_like 'projects response' do
let(:filter) { { id_after: project2.id } }
let(:current_user) { user }
- let(:projects) { [public_project, project, project2, project3].select { |p| p.id > project2.id } }
+ let(:projects) { user_projects.select { |p| p.id > project2.id } }
end
context 'regression: empty string is ignored' do
it_behaves_like 'projects response' do
let(:filter) { { id_after: '' } }
let(:current_user) { user }
- let(:projects) { [public_project, project, project2, project3] }
+ let(:projects) { user_projects }
end
end
end
@@ -402,14 +397,14 @@ RSpec.describe API::Projects do
it_behaves_like 'projects response' do
let(:filter) { { id_before: project2.id } }
let(:current_user) { user }
- let(:projects) { [public_project, project, project2, project3].select { |p| p.id < project2.id } }
+ let(:projects) { user_projects.select { |p| p.id < project2.id } }
end
context 'regression: empty string is ignored' do
it_behaves_like 'projects response' do
let(:filter) { { id_before: '' } }
let(:current_user) { user }
- let(:projects) { [public_project, project, project2, project3] }
+ let(:projects) { user_projects }
end
end
end
@@ -418,7 +413,7 @@ RSpec.describe API::Projects do
it_behaves_like 'projects response' do
let(:filter) { { id_before: project2.id, id_after: public_project.id } }
let(:current_user) { user }
- let(:projects) { [public_project, project, project2, project3].select { |p| p.id < project2.id && p.id > public_project.id } }
+ let(:projects) { user_projects.select { |p| p.id < project2.id && p.id > public_project.id } }
end
end
@@ -481,7 +476,7 @@ RSpec.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['id']).to eq(project3.id)
+ expect(json_response.map { |p| p['id'] }).to eq(user_projects.map(&:id).sort.reverse)
end
end
@@ -495,14 +490,32 @@ RSpec.describe API::Projects do
expect(json_response.first['name']).to eq(project4.name)
expect(json_response.first['owner']['username']).to eq(user4.username)
end
+
+ context 'when admin creates a project' do
+ before do
+ group = create(:group)
+ project_create_opts = {
+ name: 'GitLab',
+ namespace_id: group.id
+ }
+
+ Projects::CreateService.new(admin, project_create_opts).execute
+ end
+
+ it 'does not list as owned project for admin' do
+ get api('/projects', admin), params: { owned: true }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_empty
+ end
+ end
end
context 'and with starred=true' do
let(:public_project) { create(:project, :public) }
before do
- project_member
- user3.update(starred_projects: [project, project2, project3, public_project])
+ user3.update!(starred_projects: [project, project2, project3, public_project])
end
it 'returns the starred projects viewable by the user' do
@@ -523,7 +536,7 @@ RSpec.describe API::Projects do
let!(:project9) { create(:project, :public, path: 'gitlab9') }
before do
- user.update(starred_projects: [project5, project7, project8, project9])
+ user.update!(starred_projects: [project5, project7, project8, project9])
end
context 'including owned filter' do
@@ -642,7 +655,6 @@ RSpec.describe API::Projects do
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 }
@@ -650,7 +662,7 @@ RSpec.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.map { |project| project['id'] }).to eq(projects.map(&:id).reverse)
+ expect(json_response.map { |project| project['id'] }).to eq(user_projects.map(&:id).sort.reverse)
end
end
end
@@ -686,7 +698,8 @@ RSpec.describe API::Projects do
context 'with keyset pagination' do
let(:current_user) { user }
- let(:projects) { [public_project, project, project2, project3] }
+ let(:first_project_id) { user_projects.map(&:id).min }
+ let(:last_project_id) { user_projects.map(&:id).max }
context 'headers and records' do
let(:params) { { pagination: 'keyset', order_by: :id, sort: :asc, per_page: 1 } }
@@ -696,11 +709,11 @@ RSpec.describe API::Projects do
expect(response.header).to include('Links')
expect(response.header['Links']).to include('pagination=keyset')
- expect(response.header['Links']).to include("id_after=#{public_project.id}")
+ expect(response.header['Links']).to include("id_after=#{first_project_id}")
expect(response.header).to include('Link')
expect(response.header['Link']).to include('pagination=keyset')
- expect(response.header['Link']).to include("id_after=#{public_project.id}")
+ expect(response.header['Link']).to include("id_after=#{first_project_id}")
end
it 'contains only the first project with per_page = 1' do
@@ -708,7 +721,7 @@ RSpec.describe API::Projects do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
- expect(json_response.map { |p| p['id'] }).to contain_exactly(public_project.id)
+ expect(json_response.map { |p| p['id'] }).to contain_exactly(first_project_id)
end
it 'still includes a link if the end has reached and there is no more data after this page' do
@@ -752,11 +765,11 @@ RSpec.describe API::Projects do
expect(response.header).to include('Links')
expect(response.header['Links']).to include('pagination=keyset')
- expect(response.header['Links']).to include("id_before=#{project3.id}")
+ expect(response.header['Links']).to include("id_before=#{last_project_id}")
expect(response.header).to include('Link')
expect(response.header['Link']).to include('pagination=keyset')
- expect(response.header['Link']).to include("id_before=#{project3.id}")
+ expect(response.header['Link']).to include("id_before=#{last_project_id}")
end
it 'contains only the last project with per_page = 1' do
@@ -764,7 +777,7 @@ RSpec.describe API::Projects do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
- expect(json_response.map { |p| p['id'] }).to contain_exactly(project3.id)
+ expect(json_response.map { |p| p['id'] }).to contain_exactly(last_project_id)
end
end
@@ -793,7 +806,7 @@ RSpec.describe API::Projects do
ids += Gitlab::Json.parse(response.body).map { |p| p['id'] }
end
- expect(ids).to contain_exactly(*projects.map(&:id))
+ expect(ids).to contain_exactly(*user_projects.map(&:id))
end
end
end
@@ -814,7 +827,7 @@ RSpec.describe API::Projects do
.to change { Project.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
- project = Project.first
+ project = Project.last
expect(project.name).to eq('Foo Project')
expect(project.path).to eq('foo-project')
@@ -825,7 +838,7 @@ RSpec.describe API::Projects do
.to change { Project.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
- project = Project.first
+ project = Project.last
expect(project.name).to eq('foo_project')
expect(project.path).to eq('foo_project')
@@ -836,7 +849,7 @@ RSpec.describe API::Projects do
.to change { Project.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
- project = Project.first
+ project = Project.last
expect(project.name).to eq('Foo Project')
expect(project.path).to eq('path-project-Foo')
@@ -1232,7 +1245,7 @@ RSpec.describe API::Projects do
describe 'GET /users/:user_id/starred_projects/' do
before do
- user3.update(starred_projects: [project, project2, project3])
+ user3.update!(starred_projects: [project, project2, project3])
end
it 'returns error when user not found' do
@@ -1745,7 +1758,7 @@ RSpec.describe API::Projects do
end
it 'returns 404 when project is marked for deletion' do
- project.update(pending_delete: true)
+ project.update!(pending_delete: true)
get api("/projects/#{project.id}", user)
@@ -1985,7 +1998,8 @@ RSpec.describe API::Projects do
context 'when authenticated' do
context 'valid request' do
it_behaves_like 'project users response' do
- let(:current_user) { user }
+ let(:project) { project4 }
+ let(:current_user) { user4 }
end
end
@@ -2011,8 +2025,8 @@ RSpec.describe API::Projects do
get api("/projects/#{project.id}/users?skip_users=#{user.id}", user)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to eq(1)
- expect(json_response[0]['id']).to eq(other_user.id)
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |m| m['id'] }).not_to include(user.id)
end
end
end
@@ -2260,7 +2274,7 @@ RSpec.describe API::Projects do
end
it "returns a 400 error when sharing is disabled" do
- project.namespace.update(share_with_group_lock: true)
+ project.namespace.update!(share_with_group_lock: true)
post api("/projects/#{project.id}/share", user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER }
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -2417,7 +2431,7 @@ RSpec.describe API::Projects do
end
it 'updates visibility_level from public to private' do
- project3.update({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
+ project3.update!({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
project_param = { visibility: 'private' }
put api("/projects/#{project3.id}", user), params: project_param
@@ -2877,8 +2891,8 @@ RSpec.describe API::Projects do
let(:private_user) { create(:user, private_profile: true) }
before do
- user.update(starred_projects: [public_project])
- private_user.update(starred_projects: [public_project])
+ user.update!(starred_projects: [public_project])
+ private_user.update!(starred_projects: [public_project])
end
it 'returns not_found(404) for not existing project' do
diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb
index e2cfd87b507..e72ac002f6b 100644
--- a/spec/requests/api/pypi_packages_spec.rb
+++ b/spec/requests/api/pypi_packages_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe API::PypiPackages do
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) }
+ let_it_be(:job) { create(:ci_build, :running, user: user) }
describe 'GET /api/v4/projects/:id/packages/pypi/simple/:package_name' do
let_it_be(:package) { create(:pypi_package, project: project) }
@@ -58,6 +59,8 @@ RSpec.describe API::PypiPackages do
it_behaves_like 'deploy token for package GET requests'
+ it_behaves_like 'job token for package GET requests'
+
it_behaves_like 'rejects PyPI access with unknown project id'
end
@@ -108,6 +111,8 @@ RSpec.describe API::PypiPackages do
it_behaves_like 'deploy token for package uploads'
+ it_behaves_like 'job token for package uploads'
+
it_behaves_like 'rejects PyPI access with unknown project id'
end
@@ -117,7 +122,8 @@ RSpec.describe API::PypiPackages do
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(:requires_python) { '>=3.7' }
+ let(:base_params) { { requires_python: requires_python, 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 }
@@ -169,6 +175,19 @@ RSpec.describe API::PypiPackages do
end
end
+ context 'with required_python too big' do
+ let(:requires_python) { 'x' * 256 }
+ let(:token) { personal_access_token.token }
+ let(:user_headers) { basic_auth_header(user.username, token) }
+ let(:headers) { user_headers.merge(workhorse_header) }
+
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it_behaves_like 'process PyPi api request', :developer, :bad_request, true
+ end
+
context 'with an invalid package' do
let(:token) { personal_access_token.token }
let(:user_headers) { basic_auth_header(user.username, token) }
@@ -184,7 +203,21 @@ RSpec.describe API::PypiPackages do
it_behaves_like 'deploy token for package uploads'
+ it_behaves_like 'job token for package uploads'
+
it_behaves_like 'rejects PyPI access with unknown project id'
+
+ context 'file size above maximum limit' do
+ let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) }
+
+ before do
+ allow_next_instance_of(UploadedFile) do |uploaded_file|
+ allow(uploaded_file).to receive(:size).and_return(project.actual_limits.pypi_max_file_size + 1)
+ end
+ end
+
+ it_behaves_like 'returning response status', :bad_request
+ end
end
describe 'GET /api/v4/projects/:id/packages/pypi/files/:sha256/*file_identifier' do
@@ -247,6 +280,26 @@ RSpec.describe API::PypiPackages do
end
end
+ context 'with job token headers' do
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) }
+
+ context 'valid token' do
+ it_behaves_like 'returning response status', :success
+ end
+
+ context 'invalid token' do
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar') }
+
+ it_behaves_like 'returning response status', :success
+ end
+
+ context 'invalid user' do
+ let(:headers) { basic_auth_header('foo', job.token) }
+
+ it_behaves_like 'returning response status', :success
+ end
+ end
+
it_behaves_like 'rejects PyPI access with unknown project id'
end
end
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 1a93be98a67..af6731f3015 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -47,6 +47,17 @@ RSpec.describe API::Search do
end
end
+ shared_examples 'filter by state' do |scope:, search:|
+ it 'respects scope filtering' do
+ get api(endpoint, user), params: { scope: scope, search: search, state: state }
+
+ documents = Gitlab::Json.parse(response.body)
+
+ expect(documents.count).to eq(1)
+ expect(documents.first['state']).to eq(state)
+ end
+ end
+
describe 'GET /search' do
let(:endpoint) { '/search' }
@@ -88,42 +99,84 @@ RSpec.describe API::Search do
end
context 'for issues scope' do
- before do
- create(:issue, project: project, title: 'awesome issue')
+ context 'without filtering by state' do
+ before do
+ create(:issue, project: project, title: 'awesome issue')
- get api(endpoint, user), params: { scope: 'issues', search: 'awesome' }
- end
+ get api(endpoint, user), params: { scope: 'issues', search: 'awesome' }
+ end
- it_behaves_like 'response is correct', schema: 'public_api/v4/issues'
+ it_behaves_like 'response is correct', schema: 'public_api/v4/issues'
- it_behaves_like 'ping counters', scope: :issues
+ it_behaves_like 'ping counters', scope: :issues
- describe 'pagination' do
+ describe 'pagination' do
+ before do
+ create(:issue, project: project, title: 'another issue')
+ end
+
+ include_examples 'pagination', scope: :issues
+ end
+ end
+
+ context 'filter by state' do
before do
- create(:issue, project: project, title: 'another issue')
+ create(:issue, project: project, title: 'awesome opened issue')
+ create(:issue, :closed, project: project, title: 'awesome closed issue')
end
- include_examples 'pagination', scope: :issues
+ context 'state: opened' do
+ let(:state) { 'opened' }
+
+ include_examples 'filter by state', scope: :issues, search: 'awesome'
+ end
+
+ context 'state: closed' do
+ let(:state) { 'closed' }
+
+ include_examples 'filter by state', scope: :issues, search: 'awesome'
+ end
end
end
context 'for merge_requests scope' do
- before do
- create(:merge_request, source_project: repo_project, title: 'awesome mr')
+ context 'without filtering by state' do
+ before do
+ create(:merge_request, source_project: repo_project, title: 'awesome mr')
- get api(endpoint, user), params: { scope: 'merge_requests', search: 'awesome' }
- end
+ get api(endpoint, user), params: { scope: 'merge_requests', search: 'awesome' }
+ end
- it_behaves_like 'response is correct', schema: 'public_api/v4/merge_requests'
+ it_behaves_like 'response is correct', schema: 'public_api/v4/merge_requests'
- it_behaves_like 'ping counters', scope: :merge_requests
+ it_behaves_like 'ping counters', scope: :merge_requests
- describe 'pagination' do
+ describe 'pagination' do
+ before do
+ create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
+ end
+
+ include_examples 'pagination', scope: :merge_requests
+ end
+ end
+
+ context 'filter by state' do
before do
- create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
+ create(:merge_request, source_project: project, title: 'awesome opened mr')
+ create(:merge_request, :closed, project: project, title: 'awesome closed mr')
end
- include_examples 'pagination', scope: :merge_requests
+ context 'state: opened' do
+ let(:state) { 'opened' }
+
+ include_examples 'filter by state', scope: :merge_requests, search: 'awesome'
+ end
+
+ context 'state: closed' do
+ let(:state) { 'closed' }
+
+ include_examples 'filter by state', scope: :merge_requests, search: 'awesome'
+ end
end
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 8db0cdcbc2c..ef12f6dbed3 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -31,7 +31,6 @@ RSpec.describe API::Settings, 'Settings' do
expect(json_response['ecdsa_key_restriction']).to eq(0)
expect(json_response['ed25519_key_restriction']).to eq(0)
expect(json_response['performance_bar_allowed_group_id']).to be_nil
- expect(json_response['instance_statistics_visibility_private']).to be(false)
expect(json_response['allow_local_requests_from_hooks_and_services']).to be(false)
expect(json_response['allow_local_requests_from_web_hooks_and_services']).to be(false)
expect(json_response['allow_local_requests_from_system_hooks']).to be(true)
@@ -40,6 +39,7 @@ RSpec.describe API::Settings, 'Settings' do
expect(json_response['snippet_size_limit']).to eq(50.megabytes)
expect(json_response['spam_check_endpoint_enabled']).to be_falsey
expect(json_response['spam_check_endpoint_url']).to be_nil
+ expect(json_response['wiki_page_max_content_bytes']).to be_a(Integer)
end
end
@@ -104,7 +104,6 @@ RSpec.describe API::Settings, 'Settings' do
enforce_terms: true,
terms: 'Hello world!',
performance_bar_allowed_group_path: group.full_path,
- instance_statistics_visibility_private: true,
diff_max_patch_bytes: 150_000,
default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE,
local_markdown_version: 3,
@@ -118,7 +117,8 @@ RSpec.describe API::Settings, 'Settings' do
spam_check_endpoint_enabled: true,
spam_check_endpoint_url: 'https://example.com/spam_check',
disabled_oauth_sign_in_sources: 'unknown',
- import_sources: 'github,bitbucket'
+ import_sources: 'github,bitbucket',
+ wiki_page_max_content_bytes: 12345
}
expect(response).to have_gitlab_http_status(:ok)
@@ -146,7 +146,6 @@ RSpec.describe API::Settings, 'Settings' do
expect(json_response['enforce_terms']).to be(true)
expect(json_response['terms']).to eq('Hello world!')
expect(json_response['performance_bar_allowed_group_id']).to eq(group.id)
- expect(json_response['instance_statistics_visibility_private']).to be(true)
expect(json_response['diff_max_patch_bytes']).to eq(150_000)
expect(json_response['default_branch_protection']).to eq(Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
expect(json_response['local_markdown_version']).to eq(3)
@@ -161,6 +160,7 @@ RSpec.describe API::Settings, 'Settings' do
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))
+ expect(json_response['wiki_page_max_content_bytes']).to eq(12345)
end
end
@@ -239,8 +239,7 @@ RSpec.describe API::Settings, 'Settings' do
snowplow_collector_hostname: "snowplow.example.com",
snowplow_cookie_domain: ".example.com",
snowplow_enabled: true,
- snowplow_app_id: "app_id",
- snowplow_iglu_registry_url: 'https://example.com'
+ snowplow_app_id: "app_id"
}
end
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index 4e2f6e108eb..8d77026d26c 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -111,7 +111,7 @@ RSpec.describe API::Snippets do
end
it 'returns 404 for invalid snippet id' do
- snippet.destroy
+ snippet.destroy!
get api("/snippets/#{snippet.id}/raw", author)
@@ -201,7 +201,7 @@ RSpec.describe API::Snippets do
end
it 'returns 404 for invalid snippet id' do
- private_snippet.destroy
+ private_snippet.destroy!
subject
@@ -368,7 +368,7 @@ RSpec.describe API::Snippets do
context 'when the snippet is public' do
let(:extra_params) { { visibility: 'public' } }
- it 'rejects the shippet' do
+ it 'rejects the snippet' do
expect { subject }.not_to change { Snippet.count }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -391,19 +391,16 @@ RSpec.describe API::Snippets do
create(:personal_snippet, :repository, author: user, visibility_level: visibility_level)
end
- shared_examples 'snippet updates' do
- it 'updates a snippet' do
- new_content = 'New content'
- new_description = 'New description'
+ it_behaves_like 'snippet file updates'
+ it_behaves_like 'snippet non-file updates'
+ it_behaves_like 'snippet individual non-file updates'
+ it_behaves_like 'invalid snippet updates'
- update_snippet(params: { content: new_content, description: new_description, visibility: 'internal' })
+ it "returns 404 for another user's snippet" do
+ update_snippet(requester: other_user, params: { title: 'foobar' })
- expect(response).to have_gitlab_http_status(:ok)
- snippet.reload
- expect(snippet.content).to eq(new_content)
- expect(snippet.description).to eq(new_description)
- expect(snippet.visibility).to eq('internal')
- end
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
end
context 'with restricted visibility settings' do
@@ -413,43 +410,7 @@ RSpec.describe API::Snippets do
Gitlab::VisibilityLevel::PRIVATE])
end
- it_behaves_like 'snippet updates'
- end
-
- it_behaves_like 'snippet updates'
-
- it 'returns 404 for invalid snippet id' do
- update_snippet(snippet_id: non_existing_record_id, params: { title: 'Foo' })
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect(json_response['message']).to eq('404 Snippet Not Found')
- end
-
- it "returns 404 for another user's snippet" do
- update_snippet(requester: other_user, params: { title: 'foobar' })
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect(json_response['message']).to eq('404 Snippet Not Found')
- end
-
- it 'returns 400 for missing parameters' do
- update_snippet
-
- expect(response).to have_gitlab_http_status(:bad_request)
- end
-
- it 'returns 400 if content is blank' do
- update_snippet(params: { content: '' })
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq 'content is empty'
- end
-
- it 'returns 400 if title is blank' do
- update_snippet(params: { title: '' })
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq 'title is empty'
+ it_behaves_like 'snippet non-file updates'
end
it_behaves_like 'update with repository actions' do
@@ -475,7 +436,7 @@ RSpec.describe API::Snippets do
context 'when the snippet is public' do
let(:visibility_level) { Snippet::PUBLIC }
- it 'rejects the shippet' do
+ it 'rejects the snippet' do
expect { update_snippet(params: { title: 'Foo' }) }
.not_to change { snippet.reload.title }
diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb
index c47a12456c3..8d128bd911f 100644
--- a/spec/requests/api/terraform/state_spec.rb
+++ b/spec/requests/api/terraform/state_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe API::Terraform::State do
let_it_be(:developer) { create(:user, developer_projects: [project]) }
let_it_be(:maintainer) { create(:user, maintainer_projects: [project]) }
- let!(:state) { create(:terraform_state, :with_file, project: project) }
+ let!(:state) { create(:terraform_state, :with_version, project: project) }
let(:current_user) { maintainer }
let(:auth_header) { user_basic_auth_header(current_user) }
@@ -42,7 +42,7 @@ RSpec.describe API::Terraform::State do
request
expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to eq(state.file.read)
+ expect(response.body).to eq(state.reload.latest_file.read)
end
context 'for a project that does not exist' do
@@ -63,7 +63,7 @@ RSpec.describe API::Terraform::State do
request
expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to eq(state.file.read)
+ expect(response.body).to eq(state.reload.latest_file.read)
end
end
end
@@ -78,7 +78,7 @@ RSpec.describe API::Terraform::State do
request
expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to eq(state.file.read)
+ expect(response.body).to eq(state.reload.latest_file.read)
end
it 'returns unauthorized if the the job is not running' do
@@ -106,14 +106,14 @@ RSpec.describe API::Terraform::State do
request
expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to eq(state.file.read)
+ expect(response.body).to eq(state.reload.latest_file.read)
end
end
end
end
describe 'POST /projects/:id/terraform/state/:name' do
- let(:params) { { 'instance': 'example-instance' } }
+ let(:params) { { 'instance': 'example-instance', 'serial': '1' } }
subject(:request) { post api(state_path), headers: auth_header, as: :json, params: params }
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index dfd0e13d84c..bc315c5b3c6 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -34,6 +34,29 @@ RSpec.describe API::Todos do
end
context 'when authenticated' do
+ context 'when invalid params' do
+ context "invalid action" do
+ it 'returns 400' do
+ get api('/todos', john_doe), params: { action: 'InvalidAction' }
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context "invalid state" do
+ it 'returns 400' do
+ get api('/todos', john_doe), params: { state: 'InvalidState' }
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context "invalid type" do
+ it 'returns 400' do
+ get api('/todos', john_doe), params: { type: 'InvalidType' }
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+ end
+
it 'returns an array of pending todos for current user' do
get api('/todos', john_doe)
diff --git a/spec/requests/api/usage_data_spec.rb b/spec/requests/api/usage_data_spec.rb
new file mode 100644
index 00000000000..46dd54dcc73
--- /dev/null
+++ b/spec/requests/api/usage_data_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::UsageData do
+ let_it_be(:user) { create(:user) }
+
+ describe 'POST /usage_data/increment_unique_users' do
+ let(:endpoint) { '/usage_data/increment_unique_users' }
+ let(:known_event) { 'g_compliance_dashboard' }
+ let(:unknown_event) { 'unknown' }
+
+ context 'without CSRF token' do
+ it 'returns forbidden' do
+ stub_feature_flags(usage_data_api: true)
+ allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(false)
+
+ post api(endpoint, user), params: { event: known_event }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'usage_data_api feature not enabled' do
+ it 'returns not_found' do
+ stub_feature_flags(usage_data_api: false)
+
+ post api(endpoint, user), params: { event: known_event }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'without authentication' do
+ it 'returns 401 response' do
+ post api(endpoint), params: { event: known_event }
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'with authentication' do
+ before do
+ stub_feature_flags(usage_data_api: true)
+ stub_feature_flags("usage_data_#{known_event}" => true)
+ stub_application_setting(usage_ping_enabled: true)
+ allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(true)
+ end
+
+ context 'when event is missing from params' do
+ it 'returns bad request' do
+ post api(endpoint, user), params: {}
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'with correct params' do
+ it 'returns status ok' do
+ expect(Gitlab::Redis::HLL).to receive(:add)
+
+ post api(endpoint, user), params: { event: known_event }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with unknown event' do
+ it 'returns status ok' do
+ expect(Gitlab::Redis::HLL).not_to receive(:add)
+
+ post api(endpoint, user), params: { event: unknown_event }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 6c6497a240b..806b586ef49 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -348,6 +348,26 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
expect(response).to match_response_schema('public_api/v4/user/basics')
expect(json_response.first.keys).not_to include 'is_admin'
end
+
+ context 'exclude_internal param' do
+ let_it_be(:internal_user) { User.alert_bot }
+
+ it 'returns all users when it is not set' do
+ get api("/users?exclude_internal=false", user)
+
+ expect(response).to match_response_schema('public_api/v4/user/basics')
+ expect(response).to include_pagination_headers
+ expect(json_response.map { |u| u['id'] }).to include(internal_user.id)
+ end
+
+ it 'returns all non internal users when it is set' do
+ get api("/users?exclude_internal=true", user)
+
+ expect(response).to match_response_schema('public_api/v4/user/basics')
+ expect(response).to include_pagination_headers
+ expect(json_response.map { |u| u['id'] }).not_to include(internal_user.id)
+ end
+ end
end
context "when admin" do
@@ -894,6 +914,50 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
expect(response).to have_gitlab_http_status(:ok)
end
+ context 'updating password' do
+ def update_password(user, admin, password = User.random_password)
+ put api("/users/#{user.id}", admin), params: { password: password }
+ end
+
+ context 'admin updates their own password' do
+ it 'does not force reset on next login' do
+ update_password(admin, admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(user.reload.password_expired?).to eq(false)
+ end
+
+ it 'does not enqueue the `admin changed your password` email' do
+ expect { update_password(admin, admin) }
+ .not_to have_enqueued_mail(DeviseMailer, :password_change_by_admin)
+ end
+
+ it 'enqueues the `password changed` email' do
+ expect { update_password(admin, admin) }
+ .to have_enqueued_mail(DeviseMailer, :password_change)
+ end
+ end
+
+ context 'admin updates the password of another user' do
+ it 'forces reset on next login' do
+ update_password(user, admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(user.reload.password_expired?).to eq(true)
+ end
+
+ it 'enqueues the `admin changed your password` email' do
+ expect { update_password(user, admin) }
+ .to have_enqueued_mail(DeviseMailer, :password_change_by_admin)
+ end
+
+ it 'does not enqueue the `password changed` email' do
+ expect { update_password(user, admin) }
+ .not_to have_enqueued_mail(DeviseMailer, :password_change)
+ end
+ end
+ end
+
it "updates user with new bio" do
put api("/users/#{user.id}", admin), params: { bio: 'new test bio' }
@@ -920,13 +984,6 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
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' }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(user.reload.password_expires_at).to be <= Time.now
- end
-
it "updates user with organization" do
put api("/users/#{user.id}", admin), params: { organization: 'GitLab' }
@@ -1377,7 +1434,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do
end
end
- describe 'POST /users/:id/keys' do
+ describe 'POST /users/:id/gpg_keys' do
it 'does not create invalid GPG key' do
post api("/users/#{user.id}/gpg_keys", admin)
diff --git a/spec/requests/api/v3/github_spec.rb b/spec/requests/api/v3/github_spec.rb
new file mode 100644
index 00000000000..86ddf4a78d8
--- /dev/null
+++ b/spec/requests/api/v3/github_spec.rb
@@ -0,0 +1,516 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::V3::Github do
+ let(:user) { create(:user) }
+ let(:unauthorized_user) { create(:user) }
+ let(:admin) { create(:user, :admin) }
+ let(:project) { create(:project, :repository, creator: user) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ describe 'GET /orgs/:namespace/repos' do
+ it 'returns an empty array' do
+ group = create(:group)
+
+ jira_get v3_api("/orgs/#{group.path}/repos", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq([])
+ end
+
+ it 'returns 200 when namespace path include a dot' do
+ group = create(:group, path: 'foo.bar')
+
+ jira_get v3_api("/orgs/#{group.path}/repos", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ describe 'GET /user/repos' do
+ it 'returns an empty array' do
+ jira_get v3_api('/user/repos', user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq([])
+ end
+ end
+
+ shared_examples_for 'Jira-specific mimicked GitHub endpoints' do
+ describe 'GET /.../issues/:id/comments' do
+ let(:merge_request) do
+ create(:merge_request, source_project: project, target_project: project)
+ end
+
+ let!(:note) do
+ create(:note, project: project, noteable: merge_request)
+ end
+
+ context 'when user has access to the merge request' do
+ it 'returns an array of notes' do
+ jira_get v3_api("/repos/#{path}/issues/#{merge_request.id}/comments", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an(Array)
+ expect(json_response.size).to eq(1)
+ end
+ end
+
+ context 'when user has no access to the merge request' do
+ let(:project) { create(:project, :private) }
+
+ before do
+ project.add_guest(user)
+ end
+
+ it 'returns 404' do
+ jira_get v3_api("/repos/#{path}/issues/#{merge_request.id}/comments", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'GET /.../pulls/:id/commits' do
+ it 'returns an empty array' do
+ jira_get v3_api("/repos/#{path}/pulls/xpto/commits", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq([])
+ end
+ end
+
+ describe 'GET /.../pulls/:id/comments' do
+ it 'returns an empty array' do
+ jira_get v3_api("/repos/#{path}/pulls/xpto/comments", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq([])
+ end
+ end
+ end
+
+ # Here we test that using /-/jira as namespace/project still works,
+ # since that is how old Jira setups will talk to us
+ context 'old /-/jira endpoints' do
+ it_behaves_like 'Jira-specific mimicked GitHub endpoints' do
+ let(:path) { '-/jira' }
+ end
+
+ it 'returns an empty Array for events' do
+ jira_get v3_api('/repos/-/jira/events', user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq([])
+ end
+ end
+
+ context 'new :namespace/:project jira endpoints' do
+ it_behaves_like 'Jira-specific mimicked GitHub endpoints' do
+ let(:path) { "#{project.namespace.path}/#{project.path}" }
+ end
+
+ describe 'GET /users/:username' do
+ let!(:user1) { create(:user, username: 'jane.porter') }
+
+ context 'user exists' do
+ it 'responds with the expected user' do
+ jira_get v3_api("/users/#{user.username}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('entities/github/user')
+ end
+ end
+
+ context 'user does not exist' do
+ it 'responds with the expected status' do
+ jira_get v3_api('/users/unknown_user_name', user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'no rights to request user lists' do
+ before do
+ expect(Ability).to receive(:allowed?).with(unauthorized_user, :read_users_list, :global).and_return(false)
+ expect(Ability).to receive(:allowed?).at_least(:once).and_call_original
+ end
+
+ it 'responds with forbidden' do
+ jira_get v3_api("/users/#{user.username}", unauthorized_user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ describe 'GET events' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, :empty_repo, path: 'project.with.dot', group: group) }
+ let(:events_path) { "/repos/#{group.path}/#{project.path}/events" }
+
+ context 'if there are no merge requests' do
+ it 'returns an empty array' do
+ jira_get v3_api(events_path, user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq([])
+ end
+ end
+
+ context 'if there is a merge request' do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
+
+ it 'returns an event' do
+ jira_get v3_api(events_path, user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an(Array)
+ expect(json_response.size).to eq(1)
+ end
+ end
+
+ context 'if there are more merge requests' do
+ let!(:merge_request) { create(:merge_request, id: 10000, source_project: project, target_project: project, author: user) }
+ let!(:merge_request2) { create(:merge_request, id: 10001, source_project: project, source_branch: generate(:branch), target_project: project, author: user) }
+
+ it 'returns the expected amount of events' do
+ jira_get v3_api(events_path, user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an(Array)
+ expect(json_response.size).to eq(2)
+ end
+
+ it 'ensures each event has a unique id' do
+ jira_get v3_api(events_path, user)
+
+ ids = json_response.map { |event| event['id'] }.uniq
+ expect(ids.size).to eq(2)
+ end
+ end
+ end
+ end
+
+ describe 'repo pulls' do
+ let(:project2) { create(:project, :repository, creator: user) }
+ let(:assignee) { create(:user) }
+ let(:assignee2) { create(:user) }
+ let!(:merge_request) do
+ create(:merge_request, source_project: project, target_project: project, author: user, assignees: [assignee])
+ end
+
+ let!(:merge_request_2) do
+ create(:merge_request, source_project: project2, target_project: project2, author: user, assignees: [assignee, assignee2])
+ end
+
+ before do
+ project2.add_maintainer(user)
+ end
+
+ describe 'GET /-/jira/pulls' do
+ it 'returns an array of merge requests with github format' do
+ jira_get v3_api('/repos/-/jira/pulls', user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an(Array)
+ expect(json_response.size).to eq(2)
+ expect(response).to match_response_schema('entities/github/pull_requests')
+ end
+ end
+
+ describe 'GET /repos/:namespace/:project/pulls' do
+ it 'returns an array of merge requests for the proper project in github format' do
+ jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an(Array)
+ expect(json_response.size).to eq(1)
+ expect(response).to match_response_schema('entities/github/pull_requests')
+ end
+ end
+
+ describe 'GET /repos/:namespace/:project/pulls/:id' do
+ context 'when user has access to the merge requests' do
+ it 'returns the requested merge request in github format' do
+ jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('entities/github/pull_request')
+ end
+ end
+
+ context 'when user has no access to the merge request' do
+ it 'returns 404' do
+ project.add_guest(unauthorized_user)
+
+ jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", unauthorized_user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when instance admin' do
+ it 'returns the requested merge request in github format' do
+ jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('entities/github/pull_request')
+ end
+ end
+ end
+ end
+
+ describe 'GET /users/:namespace/repos' do
+ let(:group) { create(:group, name: 'foo') }
+
+ def expect_project_under_namespace(projects, namespace, user)
+ jira_get v3_api("/users/#{namespace.path}/repos", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(response).to match_response_schema('entities/github/repositories')
+
+ projects.each do |project|
+ hash = json_response.find do |hash|
+ hash['name'] == ::Gitlab::Jira::Dvcs.encode_project_name(project)
+ end
+
+ raise "Project #{project.full_path} not present in response" if hash.nil?
+
+ expect(hash['owner']['login']).to eq(namespace.path)
+ end
+ expect(json_response.size).to eq(projects.size)
+ end
+
+ context 'group namespace' do
+ let(:project) { create(:project, group: group) }
+ let!(:project2) { create(:project, :public, group: group) }
+
+ it 'returns an array of projects belonging to group excluding the ones user is not directly a member of, even when public' do
+ expect_project_under_namespace([project], group, user)
+ end
+
+ context 'when instance admin' do
+ let(:user) { create(:user, :admin) }
+
+ it 'returns an array of projects belonging to group' do
+ expect_project_under_namespace([project, project2], group, user)
+ end
+
+ context 'with a private group' do
+ let(:group) { create(:group, :private) }
+ let!(:project2) { create(:project, :private, group: group) }
+
+ it 'returns an array of projects belonging to group' do
+ expect_project_under_namespace([project, project2], group, user)
+ end
+ end
+ end
+ end
+
+ context 'nested group namespace' do
+ let(:group) { create(:group, :nested) }
+ let!(:parent_group_project) { create(:project, group: group.parent, name: 'parent_group_project') }
+ let!(:child_group_project) { create(:project, group: group, name: 'child_group_project') }
+
+ before do
+ group.parent.add_maintainer(user)
+ end
+
+ it 'returns an array of projects belonging to group with github format' do
+ expect_project_under_namespace([parent_group_project, child_group_project], group.parent, user)
+ end
+
+ it 'avoids N+1 queries' do
+ jira_get v3_api("/users/#{group.parent.path}/repos", user)
+
+ control = ActiveRecord::QueryRecorder.new { jira_get v3_api("/users/#{group.parent.path}/repos", user) }
+
+ new_group = create(:group, parent: group.parent)
+ create(:project, :repository, group: new_group, creator: user)
+
+ expect { jira_get v3_api("/users/#{group.parent.path}/repos", user) }.not_to exceed_query_limit(control)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'user namespace' do
+ let(:project) { create(:project, namespace: user.namespace) }
+
+ it 'returns an array of projects belonging to user namespace with github format' do
+ expect_project_under_namespace([project], user.namespace, user)
+ end
+ end
+
+ context 'namespace path includes a dot' do
+ let(:project) { create(:project, group: group) }
+ let(:group) { create(:group, name: 'foo.bar') }
+
+ before do
+ group.add_maintainer(user)
+ end
+
+ it 'returns an array of projects belonging to group with github format' do
+ expect_project_under_namespace([project], group, user)
+ end
+ end
+
+ context 'unauthenticated' do
+ it 'returns 401' do
+ jira_get v3_api('/users/foo/repos', nil)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'namespace does not exist' do
+ it 'responds with not found status' do
+ jira_get v3_api('/users/noo/repos', user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'GET /repos/:namespace/:project/branches' do
+ context 'authenticated' do
+ context 'updating project feature usage' do
+ it 'counts Jira Cloud integration as enabled' do
+ user_agent = 'Jira DVCS Connector Vertigo/4.42.0'
+
+ Timecop.freeze do
+ jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user), user_agent
+
+ expect(project.reload.jira_dvcs_cloud_last_sync_at).to be_like_time(Time.now)
+ end
+ end
+
+ it 'counts Jira Server integration as enabled' do
+ user_agent = 'Jira DVCS Connector/3.2.4'
+
+ Timecop.freeze do
+ jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user), user_agent
+
+ expect(project.reload.jira_dvcs_server_last_sync_at).to be_like_time(Time.now)
+ end
+ end
+ end
+
+ it 'returns an array of project branches with github format' do
+ jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an(Array)
+
+ expect(response).to match_response_schema('entities/github/branches')
+ end
+
+ it 'returns 200 when project path include a dot' do
+ project.update!(path: 'foo.bar')
+
+ jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'returns 200 when namespace path include a dot' do
+ group = create(:group, path: 'foo.bar')
+ project = create(:project, :repository, group: group)
+ project.add_reporter(user)
+
+ jira_get v3_api("/repos/#{group.path}/#{project.path}/branches", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'unauthenticated' do
+ it 'returns 401' do
+ jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", nil)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'unauthorized' do
+ it 'returns 404 when lower access level' do
+ project.add_guest(unauthorized_user)
+
+ jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", unauthorized_user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'GET /repos/:namespace/:project/commits/:sha' do
+ let(:commit) { project.repository.commit }
+ let(:commit_id) { commit.id }
+
+ context 'authenticated' do
+ it 'returns commit with github format' do
+ jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('entities/github/commit')
+ end
+
+ it 'returns 200 when project path include a dot' do
+ project.update!(path: 'foo.bar')
+
+ jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'returns 200 when namespace path include a dot' do
+ group = create(:group, path: 'foo.bar')
+ project = create(:project, :repository, group: group)
+ project.add_reporter(user)
+
+ jira_get v3_api("/repos/#{group.path}/#{project.path}/commits/#{commit_id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'unauthenticated' do
+ it 'returns 401' do
+ jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", nil)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'unauthorized' do
+ it 'returns 404 when lower access level' do
+ project.add_guest(unauthorized_user)
+
+ jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}",
+ unauthorized_user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ def jira_get(path, user_agent = 'Jira DVCS Connector/3.2.4')
+ get path, headers: { 'User-Agent' => user_agent }
+ end
+
+ def v3_api(path, user = nil, personal_access_token: nil, oauth_access_token: nil)
+ api(
+ path,
+ user,
+ version: 'v3',
+ personal_access_token: personal_access_token,
+ oauth_access_token: oauth_access_token
+ )
+ end
+end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index 7bb73e9664b..1ae9b0d548d 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -60,25 +60,10 @@ RSpec.describe API::Variables do
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)
+ 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
+ expect(response).to have_gitlab_http_status(:conflict)
end
end
@@ -232,25 +217,10 @@ RSpec.describe API::Variables do
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)
+ 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
+ expect(response).to have_gitlab_http_status(:conflict)
end
end
@@ -312,26 +282,10 @@ RSpec.describe API::Variables do
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 409' do
+ get api("/projects/#{project.id}/variables/key1", user)
- 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
+ expect(response).to have_gitlab_http_status(:conflict)
end
end