diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-19 08:27:35 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-19 08:27:35 +0000 |
commit | 7e9c479f7de77702622631cff2628a9c8dcbc627 (patch) | |
tree | c8f718a08e110ad7e1894510980d2155a6549197 /spec/requests/api | |
parent | e852b0ae16db4052c1c567d9efa4facc81146e88 (diff) | |
download | gitlab-ce-7e9c479f7de77702622631cff2628a9c8dcbc627.tar.gz |
Add latest changes from gitlab-org/gitlab@13-6-stable-eev13.6.0-rc42
Diffstat (limited to 'spec/requests/api')
88 files changed, 4444 insertions, 1365 deletions
diff --git a/spec/requests/api/admin/instance_clusters_spec.rb b/spec/requests/api/admin/instance_clusters_spec.rb index 9d0661089a9..1052080aad4 100644 --- a/spec/requests/api/admin/instance_clusters_spec.rb +++ b/spec/requests/api/admin/instance_clusters_spec.rb @@ -13,6 +13,7 @@ RSpec.describe ::API::Admin::InstanceClusters do user: admin_user, projects: [project]) end + let(:project_cluster_id) { project_cluster.id } describe "GET /admin/clusters" do diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb index 7d637757f38..3cc8764de4a 100644 --- a/spec/requests/api/api_spec.rb +++ b/spec/requests/api/api_spec.rb @@ -92,4 +92,36 @@ RSpec.describe API::API do end end end + + context 'application context' do + let_it_be(:project) { create(:project) } + let_it_be(:user) { project.owner } + + it 'logs all application context fields' do + allow_any_instance_of(Gitlab::GrapeLogging::Loggers::ContextLogger).to receive(:parameters) do + Labkit::Context.current.to_h.tap do |log_context| + expect(log_context).to match('correlation_id' => an_instance_of(String), + 'meta.caller_id' => '/api/:version/projects/:id/issues', + 'meta.project' => project.full_path, + 'meta.root_namespace' => project.namespace.full_path, + 'meta.user' => user.username, + 'meta.feature_category' => 'issue_tracking') + end + end + + get(api("/projects/#{project.id}/issues", user)) + end + + it 'skips fields that do not apply' do + allow_any_instance_of(Gitlab::GrapeLogging::Loggers::ContextLogger).to receive(:parameters) do + Labkit::Context.current.to_h.tap do |log_context| + expect(log_context).to match('correlation_id' => an_instance_of(String), + 'meta.caller_id' => '/api/:version/users', + 'meta.feature_category' => 'users') + end + end + + get(api('/users')) + end + end end diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb index a63198c5407..36fc6101b84 100644 --- a/spec/requests/api/boards_spec.rb +++ b/spec/requests/api/boards_spec.rb @@ -35,7 +35,46 @@ RSpec.describe API::Boards do it_behaves_like 'group and project boards', "/projects/:id/boards" - describe "POST /projects/:id/boards/lists" do + describe "POST /projects/:id/boards" do + let(:url) { "/projects/#{board_parent.id}/boards" } + + it 'creates a new issue board' do + post api(url, user), params: { name: 'foo' } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['name']).to eq('foo') + end + + it 'fails to create a new board' do + post api(url, user), params: { some_name: 'foo' } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('name is missing') + end + end + + describe "PUT /projects/:id/boards/:board_id" do + let(:url) { "/projects/#{board_parent.id}/boards/#{board.id}" } + + it 'updates the issue board' do + put api(url, user), params: { name: 'changed board name' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['name']).to eq('changed board name') + end + end + + describe "DELETE /projects/:id/boards/:board_id" do + let(:url) { "/projects/#{board_parent.id}/boards/#{board.id}" } + + it 'delete the issue board' do + delete api(url, user) + + expect(response).to have_gitlab_http_status(:no_content) + end + end + + describe "POST /projects/:id/boards/:board_id/lists" do let(:url) { "/projects/#{board_parent.id}/boards/#{board.id}/lists" } it 'creates a new issue board list for group labels' do @@ -65,7 +104,7 @@ RSpec.describe API::Boards do end end - describe "POST /groups/:id/boards/lists" do + describe "POST /groups/:id/boards/:board_id/lists" do let_it_be(:group) { create(:group) } let_it_be(:board_parent) { create(:group, parent: group ) } let(:url) { "/groups/#{board_parent.id}/boards/#{board.id}/lists" } diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb index 577b43e6e42..767b5704851 100644 --- a/spec/requests/api/ci/pipelines_spec.rb +++ b/spec/requests/api/ci/pipelines_spec.rb @@ -325,236 +325,113 @@ RSpec.describe API::Ci::Pipelines do end end - context 'with ci_jobs_finder_refactor ff enabled' do - before do - stub_feature_flags(ci_jobs_finder_refactor: true) + 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 - 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 + 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 - 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 + it_behaves_like 'a job with artifacts and trace' do + let(:api_endpoint) { "/projects/#{project.id}/pipelines/#{pipeline.id}/jobs" } end - context 'unauthorized user' do - context 'when user is not logged in' do - let(:api_user) { nil } + it 'returns pipeline data' do + json_job = json_response.first - 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 + 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 - end - context 'with ci_jobs_finder ff disabled' do - before do - stub_feature_flags(ci_jobs_finder_refactor: false) - end + context 'filter jobs with one scope element' do + let(:query) { { 'scope' => 'pending' } } - context 'authorized user' do - it 'returns pipeline jobs' do + it do expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers expect(json_response).to be_an Array end + 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 + context 'filter jobs with hash' do + let(:query) { { scope: { hello: 'pending', world: 'running' } } } - 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 + it { expect(response).to have_gitlab_http_status(:bad_request) } + end - context 'filter jobs with one scope element' do - let(:query) { { 'scope' => 'pending' } } + 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 + 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' } } } + 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 + 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) } } + context 'jobs in different pipelines' do + let!(:pipeline2) { create(:ci_empty_pipeline, project: project) } + let!(:job2) { create(:ci_build, pipeline: pipeline2) } - it do - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_an Array - end + it 'excludes jobs from other pipelines' do + json_response.each { |job| expect(job['pipeline']['id']).to eq(pipeline.id) } end + end - context 'respond 400 when scope contains invalid state' do - let(:query) { { scope: %w(unknown running) } } + 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 - it { expect(response).to have_gitlab_http_status(:bad_request) } - end + create_list(:ci_build, 3, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline) - 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 + 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 - create_list(:ci_build, 3, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline) + context 'no pipeline is found' do + it 'does not return jobs' do + get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/jobs", user) - 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 + expect(json_response['message']).to eq '404 Project Not Found' + expect(response).to have_gitlab_http_status(:not_found) end + end - context 'no pipeline is found' do - it 'does not return jobs' do - get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/jobs", user) + 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 '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 } - 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 + it 'does not return jobs' do + expect(response).to have_gitlab_http_status(:forbidden) end end end @@ -583,314 +460,152 @@ RSpec.describe API::Ci::Pipelines do end end - context 'with ci_jobs_finder_refactor ff enabled' do - before do - stub_feature_flags(ci_jobs_finder_refactor: true) + 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 - 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 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 - 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 - 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 - 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 - 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 + context 'filter bridges' do + before_all do + create_bridge(pipeline, :pending) + create_bridge(pipeline, :running) 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) } } + 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 + 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 + 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 '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 'with array of scope elements' do + let(:query) { { scope: %w(pending running) } } - context 'in a string' do - let(:query) { { scope: "unknown" } } + it :skip_before_request do + get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query - it { expect(response).to have_gitlab_http_status(:bad_request) } + 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 'bridges in different pipelines' do - let!(:pipeline2) { create(:ci_empty_pipeline, project: project) } - let!(:bridge2) { create(:ci_bridge, pipeline: pipeline2) } + context 'respond 400 when scope contains invalid state' do + context 'in an array' do + let(:query) { { scope: %w(unknown running) } } - it 'excludes bridges from other pipelines' do - json_response.each { |bridge| expect(bridge['pipeline']['id']).to eq(pipeline.id) } - end + it { expect(response).to have_gitlab_http_status(:bad_request) } 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) } + context 'in a hash' do + let(:query) { { scope: { unknown: true } } } - expect do - get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query - end.not_to exceed_all_query_limit(control_count) + it { expect(response).to have_gitlab_http_status(:bad_request) } end - end - context 'no pipeline is found' do - it 'does not return bridges' do - get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/bridges", user) + context 'in a string' do + let(:query) { { scope: "unknown" } } - expect(json_response['message']).to eq '404 Project Not Found' - expect(response).to have_gitlab_http_status(:not_found) + it { expect(response).to have_gitlab_http_status(:bad_request) } end end - context 'unauthorized user' do - context 'when user is not logged in' do - let(:api_user) { nil } + context 'bridges in different pipelines' do + let!(:pipeline2) { create(:ci_empty_pipeline, project: project) } + let!(:bridge2) { create(:ci_bridge, pipeline: pipeline2) } - 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 + it 'excludes bridges from other pipelines' do + json_response.each { |bridge| expect(bridge['pipeline']['id']).to eq(pipeline.id) } end + end - context 'when user is guest' do - let(:api_user) { guest } - let(:guest) { create(:project_member, :guest, project: project).user } + 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 - it 'does not return bridges' do - expect(response).to have_gitlab_http_status(:forbidden) - end - end + 3.times { create_bridge(pipeline) } - 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 + 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 '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 + context 'no pipeline is found' do + it 'does not return bridges' do + get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/bridges", user) - 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 + expect(json_response['message']).to eq '404 Project Not Found' + expect(response).to have_gitlab_http_status(:not_found) end + end - context 'no pipeline is found' do - it 'does not return bridges' do - get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/bridges", user) + 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 'unauthorized user' do - context 'when user is not logged in' do - let(:api_user) { nil } + 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(json_response['message']).to eq '404 Project Not Found' - expect(response).to have_gitlab_http_status(:not_found) - end + it 'does not return bridges' do + expect(response).to have_gitlab_http_status(:forbidden) 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 + context 'when user has no read_build access for project' do + before do + project.add_guest(api_user) 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 + 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 diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index d455ed9c194..de2cfb8fea0 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -1991,6 +1991,17 @@ RSpec.describe API::Commits do expect(json_response['x509_certificate']['x509_issuer']['subject']).to eq(commit.signature.x509_certificate.x509_issuer.subject) expect(json_response['x509_certificate']['x509_issuer']['subject_key_identifier']).to eq(commit.signature.x509_certificate.x509_issuer.subject_key_identifier) expect(json_response['x509_certificate']['x509_issuer']['crl_url']).to eq(commit.signature.x509_certificate.x509_issuer.crl_url) + expect(json_response['commit_source']).to eq('gitaly') + end + + context 'with Rugged enabled', :enable_rugged do + it 'returns correct JSON' do + get api(route, current_user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['signature_type']).to eq('PGP') + expect(json_response['commit_source']).to eq('rugged') + end end end end diff --git a/spec/requests/api/container_repositories_spec.rb b/spec/requests/api/container_repositories_spec.rb new file mode 100644 index 00000000000..8d7494ffce1 --- /dev/null +++ b/spec/requests/api/container_repositories_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::ContainerRepositories do + let_it_be(:project) { create(:project, :private) } + let_it_be(:reporter) { create(:user) } + let_it_be(:guest) { create(:user) } + let_it_be(:repository) { create(:container_repository, project: project) } + + let(:users) do + { + anonymous: nil, + guest: guest, + reporter: reporter + } + end + + let(:api_user) { reporter } + + before do + project.add_reporter(reporter) + project.add_guest(guest) + + stub_container_registry_config(enabled: true) + end + + describe 'GET /registry/repositories/:id' do + let(:url) { "/registry/repositories/#{repository.id}" } + + subject { get api(url, api_user) } + + it_behaves_like 'rejected container repository access', :guest, :forbidden + it_behaves_like 'rejected container repository access', :anonymous, :unauthorized + + context 'for allowed user' do + it 'returns a repository' do + subject + + expect(json_response['id']).to eq(repository.id) + expect(response.body).not_to include('tags') + end + + it 'returns a matching schema' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('registry/repository') + end + + context 'with tags param' do + let(:url) { "/registry/repositories/#{repository.id}?tags=true" } + + before do + stub_container_registry_tags(repository: repository.path, tags: %w(rootA latest), with_manifest: true) + end + + it 'returns a repository and its tags' do + subject + + expect(json_response['id']).to eq(repository.id) + expect(response.body).to include('tags') + end + end + + context 'with tags_count param' do + let(:url) { "/registry/repositories/#{repository.id}?tags_count=true" } + + before do + stub_container_registry_tags(repository: repository.path, tags: %w(rootA latest), with_manifest: true) + end + + it 'returns a repository and its tags_count' do + subject + + expect(response.body).to include('tags_count') + expect(json_response['tags_count']).to eq(2) + end + end + end + + context 'with invalid repository id' do + let(:url) { "/registry/repositories/#{non_existing_record_id}" } + + it_behaves_like 'returning response status', :not_found + end + end +end diff --git a/spec/requests/api/dependency_proxy_spec.rb b/spec/requests/api/dependency_proxy_spec.rb new file mode 100644 index 00000000000..d59f2bf06e3 --- /dev/null +++ b/spec/requests/api/dependency_proxy_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::DependencyProxy, api: true do + include ExclusiveLeaseHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:blob) { create(:dependency_proxy_blob )} + let_it_be(:group, reload: true) { blob.group } + + before do + group.add_owner(user) + stub_config(dependency_proxy: { enabled: true }) + stub_last_activity_update + group.create_dependency_proxy_setting!(enabled: true) + end + + describe 'DELETE /groups/:id/dependency_proxy/cache' do + subject { delete api("/groups/#{group.id}/dependency_proxy/cache", user) } + + context 'with feature available and enabled' do + let_it_be(:lease_key) { "dependency_proxy:delete_group_blobs:#{group.id}" } + + context 'an admin user' do + it 'deletes the blobs and returns no content' do + stub_exclusive_lease(lease_key, timeout: 1.hour) + expect(PurgeDependencyProxyCacheWorker).to receive(:perform_async) + + subject + + expect(response).to have_gitlab_http_status(:no_content) + end + + context 'called multiple times in one hour', :clean_gitlab_redis_shared_state do + it 'returns 409 with an error message' do + stub_exclusive_lease_taken(lease_key, timeout: 1.hour) + + subject + + expect(response).to have_gitlab_http_status(:conflict) + expect(response.body).to include('This request has already been made.') + end + + it 'executes service only for the first time' do + expect(PurgeDependencyProxyCacheWorker).to receive(:perform_async).once + + 2.times { subject } + end + end + end + + context 'a non-admin' do + let(:user) { create(:user) } + + before do + group.add_maintainer(user) + end + + it_behaves_like 'returning response status', :forbidden + end + end + + context 'depencency proxy is not enabled' do + before do + stub_config(dependency_proxy: { enabled: false }) + end + + it_behaves_like 'returning response status', :not_found + end + end +end diff --git a/spec/requests/api/feature_flags_user_lists_spec.rb b/spec/requests/api/feature_flags_user_lists_spec.rb index 469210040dd..e2a3f92df10 100644 --- a/spec/requests/api/feature_flags_user_lists_spec.rb +++ b/spec/requests/api/feature_flags_user_lists_spec.rb @@ -95,6 +95,39 @@ RSpec.describe API::FeatureFlagsUserLists do expect(response).to have_gitlab_http_status(:ok) expect(json_response).to eq([]) end + + context 'when filtering' do + it 'returns lists matching the search term' do + create_list(name: 'test_list', user_xids: 'user1') + create_list(name: 'list_b', user_xids: 'user1,user2,user3') + + get api("/projects/#{project.id}/feature_flags_user_lists?search=test", developer) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.map { |list| list['name'] }).to eq(['test_list']) + end + + it 'returns lists matching multiple search terms' do + create_list(name: 'test_list', user_xids: 'user1') + create_list(name: 'list_b', user_xids: 'user1,user2,user3') + create_list(name: 'test_again', user_xids: 'user1,user2,user3') + + get api("/projects/#{project.id}/feature_flags_user_lists?search=test list", developer) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.map { |list| list['name'] }).to eq(['test_list']) + end + + it 'returns all lists with no query' do + create_list(name: 'list_a', user_xids: 'user1') + create_list(name: 'list_b', user_xids: 'user1,user2,user3') + + get api("/projects/#{project.id}/feature_flags_user_lists?search=", developer) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.map { |list| list['name'] }.sort).to eq(%w[list_a list_b]) + end + end end describe 'GET /projects/:id/feature_flags_user_lists/:iid' do diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index f77f127ddc8..8cd2f00a718 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -73,18 +73,20 @@ RSpec.describe API::Files do describe "HEAD /projects/:id/repository/files/:file_path" do shared_examples_for 'repository files' do + let(:options) { {} } + it 'returns 400 when file path is invalid' do - head api(route(rouge_file_path), current_user), params: params + head api(route(rouge_file_path), current_user, **options), params: params expect(response).to have_gitlab_http_status(:bad_request) end it_behaves_like 'when path is absolute' do - subject { head api(route(absolute_path), current_user), params: params } + subject { head api(route(absolute_path), current_user, **options), params: params } end it 'returns file attributes in headers' do - head api(route(file_path), current_user), params: params + head api(route(file_path), current_user, **options), params: params expect(response).to have_gitlab_http_status(:ok) expect(response.headers['X-Gitlab-File-Path']).to eq(CGI.unescape(file_path)) @@ -98,7 +100,7 @@ RSpec.describe API::Files do file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee" params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" - head api(route(file_path), current_user), params: params + head api(route(file_path), current_user, **options), params: params expect(response).to have_gitlab_http_status(:ok) expect(response.headers['X-Gitlab-File-Name']).to eq('commit.js.coffee') @@ -107,7 +109,7 @@ RSpec.describe API::Files do context 'when mandatory params are not given' do it "responds with a 400 status" do - head api(route("any%2Ffile"), current_user) + head api(route("any%2Ffile"), current_user, **options) expect(response).to have_gitlab_http_status(:bad_request) end @@ -117,7 +119,7 @@ RSpec.describe API::Files do it "responds with a 404 status" do params[:ref] = 'master' - head api(route('app%2Fmodels%2Fapplication%2Erb'), current_user), params: params + head api(route('app%2Fmodels%2Fapplication%2Erb'), current_user, **options), params: params expect(response).to have_gitlab_http_status(:not_found) end @@ -127,7 +129,7 @@ RSpec.describe API::Files do include_context 'disabled repository' it "responds with a 403 status" do - head api(route(file_path), current_user), params: params + head api(route(file_path), current_user, **options), params: params expect(response).to have_gitlab_http_status(:forbidden) end @@ -154,8 +156,8 @@ RSpec.describe API::Files do context 'when PATs are used' do it_behaves_like 'repository files' do let(:token) { create(:personal_access_token, scopes: ['read_repository'], user: user) } - let(:current_user) { user } - let(:api_user) { { personal_access_token: token } } + let(:current_user) { nil } + let(:options) { { personal_access_token: token } } end end @@ -174,21 +176,21 @@ RSpec.describe API::Files do describe "GET /projects/:id/repository/files/:file_path" do shared_examples_for 'repository files' do - let(:api_user) { current_user } + let(:options) { {} } it 'returns 400 for invalid file path' do - get api(route(rouge_file_path), api_user), params: params + get api(route(rouge_file_path), api_user, **options), params: params expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq(invalid_file_message) end it_behaves_like 'when path is absolute' do - subject { get api(route(absolute_path), api_user), params: params } + subject { get api(route(absolute_path), api_user, **options), params: params } end it 'returns file attributes as json' do - get api(route(file_path), api_user), params: params + get api(route(file_path), api_user, **options), params: params expect(response).to have_gitlab_http_status(:ok) expect(json_response['file_path']).to eq(CGI.unescape(file_path)) @@ -201,10 +203,10 @@ RSpec.describe API::Files do it 'returns json when file has txt extension' do file_path = "bar%2Fbranch-test.txt" - get api(route(file_path), api_user), params: params + get api(route(file_path), api_user, **options), params: params expect(response).to have_gitlab_http_status(:ok) - expect(response.content_type).to eq('application/json') + expect(response.media_type).to eq('application/json') end context 'with filename with pathspec characters' do @@ -218,7 +220,7 @@ RSpec.describe API::Files do it 'returns JSON wth commit SHA' do params[:ref] = 'master' - get api(route(file_path), api_user), params: params + get api(route(file_path), api_user, **options), params: params expect(response).to have_gitlab_http_status(:ok) expect(json_response['file_path']).to eq(file_path) @@ -232,7 +234,7 @@ RSpec.describe API::Files do file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee" params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" - get api(route(file_path), api_user), params: params + get api(route(file_path), api_user, **options), params: params expect(response).to have_gitlab_http_status(:ok) expect(json_response['file_name']).to eq('commit.js.coffee') @@ -244,7 +246,7 @@ RSpec.describe API::Files do url = route(file_path) + "/raw" expect(Gitlab::Workhorse).to receive(:send_git_blob) - get api(url, api_user), params: params + get api(url, api_user, **options), params: params expect(response).to have_gitlab_http_status(:ok) expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" @@ -253,7 +255,7 @@ RSpec.describe API::Files do it 'returns blame file info' do url = route(file_path) + '/blame' - get api(url, api_user), params: params + get api(url, api_user, **options), params: params expect(response).to have_gitlab_http_status(:ok) end @@ -261,14 +263,14 @@ RSpec.describe API::Files do it 'sets inline content disposition by default' do url = route(file_path) + "/raw" - get api(url, api_user), params: params + get api(url, api_user, **options), params: params expect(headers['Content-Disposition']).to eq(%q(inline; filename="popen.rb"; filename*=UTF-8''popen.rb)) end context 'when mandatory params are not given' do it_behaves_like '400 response' do - let(:request) { get api(route("any%2Ffile"), current_user) } + let(:request) { get api(route("any%2Ffile"), current_user, **options) } end end @@ -276,7 +278,7 @@ RSpec.describe API::Files do let(:params) { { ref: 'master' } } it_behaves_like '404 response' do - let(:request) { get api(route('app%2Fmodels%2Fapplication%2Erb'), api_user), params: params } + let(:request) { get api(route('app%2Fmodels%2Fapplication%2Erb'), api_user, **options), params: params } let(:message) { '404 File Not Found' } end end @@ -285,7 +287,7 @@ RSpec.describe API::Files do include_context 'disabled repository' it_behaves_like '403 response' do - let(:request) { get api(route(file_path), api_user), params: params } + let(:request) { get api(route(file_path), api_user, **options), params: params } end end end @@ -294,6 +296,7 @@ RSpec.describe API::Files do it_behaves_like 'repository files' do let(:project) { create(:project, :public, :repository) } let(:current_user) { nil } + let(:api_user) { nil } end end @@ -301,7 +304,8 @@ RSpec.describe API::Files do it_behaves_like 'repository files' do let(:token) { create(:personal_access_token, scopes: ['read_repository'], user: user) } let(:current_user) { user } - let(:api_user) { { personal_access_token: token } } + let(:api_user) { nil } + let(:options) { { personal_access_token: token } } end end @@ -315,6 +319,7 @@ RSpec.describe API::Files do context 'when authenticated', 'as a developer' do it_behaves_like 'repository files' do let(:current_user) { user } + let(:api_user) { user } end end @@ -532,13 +537,16 @@ RSpec.describe API::Files do expect(response).to have_gitlab_http_status(:ok) end - it_behaves_like 'uncached response' do - before do - url = route('.gitignore') + "/raw" - expect(Gitlab::Workhorse).to receive(:send_git_blob) + it 'sets no-cache headers' do + url = route('.gitignore') + "/raw" + expect(Gitlab::Workhorse).to receive(:send_git_blob) - get api(url, current_user), params: params - end + get api(url, current_user), params: params + + expect(response.headers["Cache-Control"]).to include("no-store") + expect(response.headers["Cache-Control"]).to include("no-cache") + expect(response.headers["Pragma"]).to eq("no-cache") + expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT") end context 'when mandatory params are not given' do @@ -687,7 +695,7 @@ RSpec.describe API::Files do post api(route("new_file_with_author%2Etxt"), user), params: params expect(response).to have_gitlab_http_status(:created) - expect(response.content_type).to eq('application/json') + expect(response.media_type).to eq('application/json') last_commit = project.repository.commit.raw expect(last_commit.author_email).to eq(author_email) expect(last_commit.author_name).to eq(author_name) diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb index 2cb686167f1..b8e79853486 100644 --- a/spec/requests/api/generic_packages_spec.rb +++ b/spec/requests/api/generic_packages_spec.rb @@ -195,7 +195,7 @@ RSpec.describe API::GenericPackages do package = project.packages.generic.last expect(package.name).to eq('mypackage') expect(package.version).to eq('0.0.1') - expect(package.build_info).to be_nil + expect(package.original_build_info).to be_nil package_file = package.package_files.last expect(package_file.file_name).to eq('myfile.tar.gz') @@ -215,7 +215,7 @@ RSpec.describe API::GenericPackages do package = project.packages.generic.last expect(package.name).to eq('mypackage') expect(package.version).to eq('0.0.1') - expect(package.build_info.pipeline).to eq(ci_build.pipeline) + expect(package.original_build_info.pipeline).to eq(ci_build.pipeline) package_file = package.package_files.last expect(package_file.file_name).to eq('myfile.tar.gz') diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb index 7d416f4720b..618705e5f94 100644 --- a/spec/requests/api/graphql/ci/jobs_spec.rb +++ b/spec/requests/api/graphql/ci/jobs_spec.rb @@ -34,6 +34,9 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do jobs { nodes { name + pipeline { + id + } } } } @@ -53,7 +56,7 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do end context 'when fetching jobs from the pipeline' do - it 'avoids N+1 queries' do + it 'avoids N+1 queries', :aggregate_failures do control_count = ActiveRecord::QueryRecorder.new do post_graphql(query, current_user: user) end @@ -86,8 +89,27 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do docker_jobs = docker_group.dig('jobs', 'nodes') rspec_jobs = rspec_group.dig('jobs', 'nodes') - expect(docker_jobs).to eq([{ 'name' => 'docker 1 2' }, { 'name' => 'docker 2 2' }]) - expect(rspec_jobs).to eq([{ 'name' => 'rspec 1 2' }, { 'name' => 'rspec 2 2' }]) + expect(docker_jobs).to eq([ + { + 'name' => 'docker 1 2', + 'pipeline' => { 'id' => pipeline.to_global_id.to_s } + }, + { + 'name' => 'docker 2 2', + 'pipeline' => { 'id' => pipeline.to_global_id.to_s } + } + ]) + + expect(rspec_jobs).to eq([ + { + 'name' => 'rspec 1 2', + 'pipeline' => { 'id' => pipeline.to_global_id.to_s } + }, + { + 'name' => 'rspec 2 2', + 'pipeline' => { 'id' => pipeline.to_global_id.to_s } + } + ]) end end end diff --git a/spec/requests/api/graphql/ci/pipelines_spec.rb b/spec/requests/api/graphql/ci/pipelines_spec.rb new file mode 100644 index 00000000000..414ddabbac9 --- /dev/null +++ b/spec/requests/api/graphql/ci/pipelines_spec.rb @@ -0,0 +1,221 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.project(fullPath).pipelines' do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository, :public) } + let_it_be(:first_user) { create(:user) } + let_it_be(:second_user) { create(:user) } + + describe '.jobs' do + let_it_be(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + pipelines { + nodes { + jobs { + nodes { + name + } + } + } + } + } + } + ) + end + + it 'fetches the jobs without an N+1' do + pipeline = create(:ci_pipeline, project: project) + create(:ci_build, pipeline: pipeline, name: 'Job 1') + + control_count = ActiveRecord::QueryRecorder.new do + post_graphql(query, current_user: first_user) + end + + pipeline = create(:ci_pipeline, project: project) + create(:ci_build, pipeline: pipeline, name: 'Job 2') + + expect do + post_graphql(query, current_user: second_user) + end.not_to exceed_query_limit(control_count) + + expect(response).to have_gitlab_http_status(:ok) + + pipelines_data = graphql_data.dig('project', 'pipelines', 'nodes') + + job_names = pipelines_data.map do |pipeline_data| + jobs_data = pipeline_data.dig('jobs', 'nodes') + jobs_data.map { |job_data| job_data['name'] } + end.flatten + + expect(job_names).to contain_exactly('Job 1', 'Job 2') + end + end + + describe '.jobs(securityReportTypes)' do + let_it_be(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + pipelines { + nodes { + jobs(securityReportTypes: [SAST]) { + nodes { + name + } + } + } + } + } + } + ) + end + + it 'fetches the jobs matching the report type filter' do + pipeline = create(:ci_pipeline, project: project) + create(:ci_build, :dast, name: 'DAST Job 1', pipeline: pipeline) + create(:ci_build, :sast, name: 'SAST Job 1', pipeline: pipeline) + + post_graphql(query, current_user: first_user) + + expect(response).to have_gitlab_http_status(:ok) + + pipelines_data = graphql_data.dig('project', 'pipelines', 'nodes') + + job_names = pipelines_data.map do |pipeline_data| + jobs_data = pipeline_data.dig('jobs', 'nodes') + jobs_data.map { |job_data| job_data['name'] } + end.flatten + + expect(job_names).to contain_exactly('SAST Job 1') + end + end + + describe 'upstream' do + let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: first_user) } + let_it_be(:upstream_project) { create(:project, :repository, :public) } + let_it_be(:upstream_pipeline) { create(:ci_pipeline, project: upstream_project, user: first_user) } + let(:upstream_pipelines_graphql_data) { graphql_data.dig(*%w[project pipelines nodes]).first['upstream'] } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + pipelines { + nodes { + upstream { + iid + } + } + } + } + } + ) + end + + before do + create(:ci_sources_pipeline, source_pipeline: upstream_pipeline, pipeline: pipeline ) + + post_graphql(query, current_user: first_user) + end + + it_behaves_like 'a working graphql query' + + it 'returns the upstream pipeline of a pipeline' do + expect(upstream_pipelines_graphql_data['iid'].to_i).to eq(upstream_pipeline.iid) + end + + context 'when fetching the upstream pipeline from the pipeline' do + it 'avoids N+1 queries' do + control_count = ActiveRecord::QueryRecorder.new do + post_graphql(query, current_user: first_user) + end + + pipeline_2 = create(:ci_pipeline, project: project, user: first_user) + upstream_pipeline_2 = create(:ci_pipeline, project: upstream_project, user: first_user) + create(:ci_sources_pipeline, source_pipeline: upstream_pipeline_2, pipeline: pipeline_2 ) + pipeline_3 = create(:ci_pipeline, project: project, user: first_user) + upstream_pipeline_3 = create(:ci_pipeline, project: upstream_project, user: first_user) + create(:ci_sources_pipeline, source_pipeline: upstream_pipeline_3, pipeline: pipeline_3 ) + + expect do + post_graphql(query, current_user: second_user) + end.not_to exceed_query_limit(control_count) + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + describe 'downstream' do + let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: first_user) } + let(:pipeline_2) { create(:ci_pipeline, project: project, user: first_user) } + + let_it_be(:downstream_project) { create(:project, :repository, :public) } + let_it_be(:downstream_pipeline_a) { create(:ci_pipeline, project: downstream_project, user: first_user) } + let_it_be(:downstream_pipeline_b) { create(:ci_pipeline, project: downstream_project, user: first_user) } + + let(:pipelines_graphql_data) { graphql_data.dig(*%w[project pipelines nodes]) } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + pipelines { + nodes { + downstream { + nodes { + iid + } + } + } + } + } + } + ) + end + + before do + create(:ci_sources_pipeline, source_pipeline: pipeline, pipeline: downstream_pipeline_a) + create(:ci_sources_pipeline, source_pipeline: pipeline_2, pipeline: downstream_pipeline_b) + + post_graphql(query, current_user: first_user) + end + + it_behaves_like 'a working graphql query' + + it 'returns the downstream pipelines of a pipeline' do + downstream_pipelines_graphql_data = pipelines_graphql_data.map { |pip| pip['downstream']['nodes'] }.flatten + + expect( + downstream_pipelines_graphql_data.map { |pip| pip['iid'].to_i } + ).to contain_exactly(downstream_pipeline_a.iid, downstream_pipeline_b.iid) + end + + context 'when fetching the downstream pipelines from the pipeline' do + it 'avoids N+1 queries' do + control_count = ActiveRecord::QueryRecorder.new do + post_graphql(query, current_user: first_user) + end + + downstream_pipeline_2a = create(:ci_pipeline, project: downstream_project, user: first_user) + create(:ci_sources_pipeline, source_pipeline: pipeline, pipeline: downstream_pipeline_2a) + downsteam_pipeline_3a = create(:ci_pipeline, project: downstream_project, user: first_user) + create(:ci_sources_pipeline, source_pipeline: pipeline, pipeline: downsteam_pipeline_3a) + + downstream_pipeline_2b = create(:ci_pipeline, project: downstream_project, user: first_user) + create(:ci_sources_pipeline, source_pipeline: pipeline_2, pipeline: downstream_pipeline_2b) + downsteam_pipeline_3b = create(:ci_pipeline, project: downstream_project, user: first_user) + create(:ci_sources_pipeline, source_pipeline: pipeline_2, pipeline: downsteam_pipeline_3b) + + expect do + post_graphql(query, current_user: second_user) + end.not_to exceed_query_limit(control_count) + end + end + end +end diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb new file mode 100644 index 00000000000..3c1c63c1670 --- /dev/null +++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'container repository details' do + using RSpec::Parameterized::TableSyntax + include GraphqlHelpers + + let_it_be_with_reload(:project) { create(:project) } + let_it_be(:container_repository) { create(:container_repository, project: project) } + + let(:query) do + graphql_query_for( + 'containerRepository', + { id: container_repository_global_id }, + all_graphql_fields_for('ContainerRepositoryDetails') + ) + end + + let(:user) { project.owner } + let(:variables) { {} } + let(:tags) { %w(latest tag1 tag2 tag3 tag4 tag5) } + let(:container_repository_global_id) { container_repository.to_global_id.to_s } + let(:container_repository_details_response) { graphql_data.dig('containerRepository') } + + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags(repository: container_repository.path, tags: tags, with_manifest: true) + end + + subject { post_graphql(query, current_user: user, variables: variables) } + + it_behaves_like 'a working graphql query' do + before do + subject + end + + it 'matches the JSON schema' do + expect(container_repository_details_response).to match_schema('graphql/container_repository_details') + end + end + + context 'with different permissions' do + let_it_be(:user) { create(:user) } + + let(:tags_response) { container_repository_details_response.dig('tags', 'nodes') } + + where(:project_visibility, :role, :access_granted, :can_delete) do + :private | :maintainer | true | true + :private | :developer | true | true + :private | :reporter | true | false + :private | :guest | false | false + :private | :anonymous | false | false + :public | :maintainer | true | true + :public | :developer | true | true + :public | :reporter | true | false + :public | :guest | true | false + :public | :anonymous | true | false + end + + with_them do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility.to_s.upcase, false)) + project.add_user(user, role) unless role == :anonymous + end + + it 'return the proper response' do + subject + + if access_granted + expect(tags_response.size).to eq(tags.size) + expect(container_repository_details_response.dig('canDelete')).to eq(can_delete) + else + expect(container_repository_details_response).to eq(nil) + end + end + end + end + + context 'limiting the number of tags' do + let(:limit) { 2 } + let(:tags_response) { container_repository_details_response.dig('tags', 'edges') } + let(:variables) do + { id: container_repository_global_id, n: limit } + end + + let(:query) do + <<~GQL + query($id: ID!, $n: Int) { + containerRepository(id: $id) { + tags(first: $n) { + edges { + node { + #{all_graphql_fields_for('ContainerRepositoryTag')} + } + } + } + } + } + GQL + end + + it 'only returns n tags' do + subject + + expect(tags_response.size).to eq(limit) + end + end +end diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb new file mode 100644 index 00000000000..d5a423d0eba --- /dev/null +++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting custom emoji within namespace' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group, :private) } + let_it_be(:custom_emoji) { create(:custom_emoji, group: group) } + + before do + stub_feature_flags(custom_emoji: true) + group.add_developer(current_user) + end + + describe "Query CustomEmoji on Group" do + def custom_emoji_query(group) + graphql_query_for('group', 'fullPath' => group.full_path) + end + + it 'returns emojis when authorised' do + post_graphql(custom_emoji_query(group), current_user: current_user) + + expect(response).to have_gitlab_http_status(:ok) + expect(graphql_data['group']['customEmoji']['nodes'].count). to eq(1) + expect(graphql_data['group']['customEmoji']['nodes'].first['name']). to eq(custom_emoji.name) + end + + it 'returns nil when unauthorised' do + user = create(:user) + post_graphql(custom_emoji_query(group), current_user: user) + + expect(graphql_data['group']).to be_nil + end + end +end diff --git a/spec/requests/api/graphql/group/container_repositories_spec.rb b/spec/requests/api/graphql/group/container_repositories_spec.rb new file mode 100644 index 00000000000..bcf689a5e8f --- /dev/null +++ b/spec/requests/api/graphql/group/container_repositories_spec.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'getting container repositories in a group' do + using RSpec::Parameterized::TableSyntax + include GraphqlHelpers + + let_it_be(:owner) { create(:user) } + let_it_be_with_reload(:group) { create(:group) } + let_it_be_with_reload(:project) { create(:project, group: group) } + let_it_be(:container_repository) { create(:container_repository, project: project) } + let_it_be(:container_repositories_delete_scheduled) { create_list(:container_repository, 2, :status_delete_scheduled, project: project) } + let_it_be(:container_repositories_delete_failed) { create_list(:container_repository, 2, :status_delete_failed, project: project) } + let_it_be(:container_repositories) { [container_repository, container_repositories_delete_scheduled, container_repositories_delete_failed].flatten } + let_it_be(:container_expiration_policy) { project.container_expiration_policy } + + let(:fields) do + <<~GQL + edges { + node { + #{all_graphql_fields_for('container_repositories'.classify)} + } + } + GQL + end + + let(:query) do + graphql_query_for( + 'group', + { 'fullPath' => group.full_path }, + query_graphql_field('container_repositories', {}, fields) + ) + end + + let(:user) { owner } + let(:variables) { {} } + let(:container_repositories_response) { graphql_data.dig('group', 'containerRepositories', 'edges') } + + before do + group.add_owner(owner) + stub_container_registry_config(enabled: true) + container_repositories.each do |repository| + stub_container_registry_tags(repository: repository.path, tags: %w(tag1 tag2 tag3), with_manifest: false) + end + end + + subject { post_graphql(query, current_user: user, variables: variables) } + + it_behaves_like 'a working graphql query' do + before do + subject + end + end + + context 'with different permissions' do + let_it_be(:user) { create(:user) } + + where(:group_visibility, :role, :access_granted, :can_delete) do + :private | :maintainer | true | true + :private | :developer | true | true + :private | :reporter | true | false + :private | :guest | false | false + :private | :anonymous | false | false + :public | :maintainer | true | true + :public | :developer | true | true + :public | :reporter | true | false + :public | :guest | false | false + :public | :anonymous | false | false + end + + with_them do + before do + group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false)) + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false)) + + group.add_user(user, role) unless role == :anonymous + end + + it 'return the proper response' do + subject + + if access_granted + expect(container_repositories_response.size).to eq(container_repositories.size) + container_repositories_response.each do |repository_response| + expect(repository_response.dig('node', 'canDelete')).to eq(can_delete) + end + else + expect(container_repositories_response).to eq(nil) + end + end + end + end + + context 'limiting the number of repositories' do + let(:limit) { 1 } + let(:variables) do + { path: group.full_path, n: limit } + end + + let(:query) do + <<~GQL + query($path: ID!, $n: Int) { + group(fullPath: $path) { + containerRepositories(first: $n) { #{fields} } + } + } + GQL + end + + it 'only returns N repositories' do + subject + + expect(container_repositories_response.size).to eq(limit) + end + end + + context 'filter by name' do + let_it_be(:container_repository) { create(:container_repository, name: 'fooBar', project: project) } + + let(:name) { 'ooba' } + let(:query) do + <<~GQL + query($path: ID!, $name: String) { + group(fullPath: $path) { + containerRepositories(name: $name) { #{fields} } + } + } + GQL + end + + let(:variables) do + { path: group.full_path, name: name } + end + + before do + stub_container_registry_tags(repository: container_repository.path, tags: %w(tag4 tag5 tag6), with_manifest: false) + end + + it 'returns the searched container repository' do + subject + + expect(container_repositories_response.size).to eq(1) + expect(container_repositories_response.first.dig('node', 'id')).to eq(container_repository.to_global_id.to_s) + end + end +end diff --git a/spec/requests/api/graphql/instance_statistics_measurements_spec.rb b/spec/requests/api/graphql/instance_statistics_measurements_spec.rb index 5d7dbcf2e3c..eb73dc59253 100644 --- a/spec/requests/api/graphql/instance_statistics_measurements_spec.rb +++ b/spec/requests/api/graphql/instance_statistics_measurements_spec.rb @@ -9,7 +9,8 @@ RSpec.describe 'InstanceStatisticsMeasurements' do 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 identifier }') } + let(:arguments) { 'identifier: PROJECTS' } + let(:query) { graphql_query_for(:instanceStatisticsMeasurements, arguments, 'nodes { count identifier }') } before do post_graphql(query, current_user: current_user) @@ -21,4 +22,14 @@ RSpec.describe 'InstanceStatisticsMeasurements' do { "count" => 5, 'identifier' => 'PROJECTS' } ]) end + + context 'with recorded_at filters' do + let(:arguments) { %(identifier: PROJECTS, recordedAfter: "#{15.days.ago.to_date}", recordedBefore: "#{5.days.ago.to_date}") } + + it 'returns filtered measurement objects' do + expect(graphql_data.dig('instanceStatisticsMeasurements', 'nodes')).to eq([ + { "count" => 10, 'identifier' => 'PROJECTS' } + ]) + end + end end diff --git a/spec/requests/api/graphql/issue/issue_spec.rb b/spec/requests/api/graphql/issue/issue_spec.rb index 1c9d6b25856..d7fa680d29b 100644 --- a/spec/requests/api/graphql/issue/issue_spec.rb +++ b/spec/requests/api/graphql/issue/issue_spec.rb @@ -71,14 +71,34 @@ RSpec.describe 'Query.issue(id)' do end context 'selecting multiple fields' do - let(:issue_fields) { %w(title description) } + let(:issue_fields) { ['title', 'description', 'updatedBy { username }'] } 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.keys).to eq( %w(title description updatedBy) ) expect(issue_data['title']).to eq(issue.title) expect(issue_data['description']).to eq(issue.description) + expect(issue_data['updatedBy']['username']).to eq(issue.author.username) + end + end + + context 'when issue got moved' do + let_it_be(:issue_fields) { ['moved', 'movedTo { title }'] } + let_it_be(:new_issue) { create(:issue) } + let_it_be(:issue) { create(:issue, project: project, moved_to: new_issue) } + let_it_be(:issue_params) { { 'id' => issue.to_global_id.to_s } } + + before_all do + new_issue.project.add_developer(current_user) + end + + it 'returns correct attributes' do + post_graphql(query, current_user: current_user) + + expect(issue_data.keys).to eq( %w(moved movedTo) ) + expect(issue_data['moved']).to eq(true) + expect(issue_data['movedTo']['title']).to eq(new_issue.title) end end diff --git a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb index ca5a9165760..72ec2b8e070 100644 --- a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb +++ b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb @@ -17,6 +17,7 @@ RSpec.describe 'Getting Metrics Dashboard Annotations' do let_it_be(:to_old_annotation) do create(:metrics_dashboard_annotation, environment: environment, starting_at: Time.parse(from).advance(minutes: -5), dashboard_path: path) end + let_it_be(:to_new_annotation) do create(:metrics_dashboard_annotation, environment: environment, starting_at: to.advance(minutes: 5), dashboard_path: path) end diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb new file mode 100644 index 00000000000..a285cebc805 --- /dev/null +++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Creating a new HTTP Integration' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project) } + let(:variables) do + { + project_path: project.full_path, + active: false, + name: 'New HTTP Integration' + } + end + + let(:mutation) do + graphql_mutation(:http_integration_create, variables) do + <<~QL + clientMutationId + errors + integration { + id + type + name + active + token + url + apiUrl + } + QL + end + end + + let(:mutation_response) { graphql_mutation_response(:http_integration_create) } + + before do + project.add_maintainer(current_user) + end + + it 'creates a new integration' do + post_graphql_mutation(mutation, current_user: current_user) + + new_integration = ::AlertManagement::HttpIntegration.last! + integration_response = mutation_response['integration'] + + expect(response).to have_gitlab_http_status(:success) + expect(integration_response['id']).to eq(GitlabSchema.id_from_object(new_integration).to_s) + expect(integration_response['type']).to eq('HTTP') + expect(integration_response['name']).to eq(new_integration.name) + expect(integration_response['active']).to eq(new_integration.active) + expect(integration_response['token']).to eq(new_integration.token) + expect(integration_response['url']).to eq(new_integration.url) + expect(integration_response['apiUrl']).to eq(nil) + end + + [:project_path, :active, :name].each do |argument| + context "without required argument #{argument}" do + before do + variables.delete(argument) + end + + it_behaves_like 'an invalid argument to the mutation', argument_name: argument + end + end +end diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb new file mode 100644 index 00000000000..1ecb5c76b57 --- /dev/null +++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Removing an HTTP Integration' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:integration) { create(:alert_management_http_integration, project: project) } + + let(:mutation) do + variables = { + id: GitlabSchema.id_from_object(integration).to_s + } + graphql_mutation(:http_integration_destroy, variables) do + <<~QL + clientMutationId + errors + integration { + id + type + name + active + token + url + apiUrl + } + QL + end + end + + let(:mutation_response) { graphql_mutation_response(:http_integration_destroy) } + + before do + project.add_maintainer(user) + end + + it 'removes the integration' do + post_graphql_mutation(mutation, current_user: user) + + integration_response = mutation_response['integration'] + + expect(response).to have_gitlab_http_status(:success) + expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s) + expect(integration_response['type']).to eq('HTTP') + expect(integration_response['name']).to eq(integration.name) + expect(integration_response['active']).to eq(integration.active) + expect(integration_response['token']).to eq(integration.token) + expect(integration_response['url']).to eq(integration.url) + expect(integration_response['apiUrl']).to eq(nil) + + expect { integration.reload }.to raise_error ActiveRecord::RecordNotFound + end +end diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb new file mode 100644 index 00000000000..badd9412589 --- /dev/null +++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/reset_token_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Resetting a token on an existing HTTP Integration' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:integration) { create(:alert_management_http_integration, project: project) } + + let(:mutation) do + variables = { + id: GitlabSchema.id_from_object(integration).to_s + } + graphql_mutation(:http_integration_reset_token, variables) do + <<~QL + clientMutationId + errors + integration { + id + token + } + QL + end + end + + let(:mutation_response) { graphql_mutation_response(:http_integration_reset_token) } + + before do + project.add_maintainer(user) + end + + it 'updates the integration' do + previous_token = integration.token + + post_graphql_mutation(mutation, current_user: user) + integration_response = mutation_response['integration'] + + expect(response).to have_gitlab_http_status(:success) + expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s) + expect(integration_response['token']).not_to eq(previous_token) + expect(integration_response['token']).to eq(integration.reload.token) + end +end diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb new file mode 100644 index 00000000000..bf7eb3d980c --- /dev/null +++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Updating an existing HTTP Integration' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:integration) { create(:alert_management_http_integration, project: project) } + + let(:mutation) do + variables = { + id: GitlabSchema.id_from_object(integration).to_s, + name: 'Modified Name', + active: false + } + graphql_mutation(:http_integration_update, variables) do + <<~QL + clientMutationId + errors + integration { + id + name + active + url + } + QL + end + end + + let(:mutation_response) { graphql_mutation_response(:http_integration_update) } + + before do + project.add_maintainer(user) + end + + it 'updates the integration' do + post_graphql_mutation(mutation, current_user: user) + + integration_response = mutation_response['integration'] + + expect(response).to have_gitlab_http_status(:success) + expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s) + expect(integration_response['name']).to eq('Modified Name') + expect(integration_response['active']).to be_falsey + expect(integration_response['url']).to include('modified-name') + end +end diff --git a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb new file mode 100644 index 00000000000..0ef61ae0d5b --- /dev/null +++ b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/create_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Creating a new Prometheus Integration' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project) } + let(:variables) do + { + project_path: project.full_path, + active: false, + api_url: 'https://prometheus-url.com' + } + end + + let(:mutation) do + graphql_mutation(:prometheus_integration_create, variables) do + <<~QL + clientMutationId + errors + integration { + id + type + name + active + token + url + apiUrl + } + QL + end + end + + let(:mutation_response) { graphql_mutation_response(:prometheus_integration_create) } + + before do + project.add_maintainer(current_user) + end + + it 'creates a new integration' do + post_graphql_mutation(mutation, current_user: current_user) + + new_integration = ::PrometheusService.last! + integration_response = mutation_response['integration'] + + expect(response).to have_gitlab_http_status(:success) + expect(integration_response['id']).to eq(GitlabSchema.id_from_object(new_integration).to_s) + expect(integration_response['type']).to eq('PROMETHEUS') + expect(integration_response['name']).to eq(new_integration.title) + expect(integration_response['active']).to eq(new_integration.manual_configuration?) + expect(integration_response['token']).to eq(new_integration.project.alerting_setting.token) + expect(integration_response['url']).to eq("http://localhost/#{project.full_path}/prometheus/alerts/notify.json") + expect(integration_response['apiUrl']).to eq(new_integration.api_url) + end + + [:project_path, :active, :api_url].each do |argument| + context "without required argument #{argument}" do + before do + variables.delete(argument) + end + + it_behaves_like 'an invalid argument to the mutation', argument_name: argument + end + end +end diff --git a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb new file mode 100644 index 00000000000..d8d0ace5981 --- /dev/null +++ b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Resetting a token on an existing Prometheus Integration' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:integration) { create(:prometheus_service, project: project) } + + let(:mutation) do + variables = { + id: GitlabSchema.id_from_object(integration).to_s + } + graphql_mutation(:prometheus_integration_reset_token, variables) do + <<~QL + clientMutationId + errors + integration { + id + token + } + QL + end + end + + let(:mutation_response) { graphql_mutation_response(:prometheus_integration_reset_token) } + + before do + project.add_maintainer(user) + end + + it 'creates a token' do + post_graphql_mutation(mutation, current_user: user) + integration_response = mutation_response['integration'] + + expect(response).to have_gitlab_http_status(:success) + expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s) + expect(integration_response['token']).not_to be_nil + expect(integration_response['token']).to eq(project.alerting_setting.token) + end + + context 'with an existing alerting setting' do + let_it_be(:alerting_setting) { create(:project_alerting_setting, project: project) } + + it 'updates the token' do + previous_token = alerting_setting.token + + post_graphql_mutation(mutation, current_user: user) + integration_response = mutation_response['integration'] + + expect(response).to have_gitlab_http_status(:success) + expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s) + expect(integration_response['token']).not_to eq(previous_token) + expect(integration_response['token']).to eq(alerting_setting.reload.token) + end + end +end diff --git a/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb new file mode 100644 index 00000000000..6c4a647a353 --- /dev/null +++ b/spec/requests/api/graphql/mutations/alert_management/prometheus_integration/update_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Updating an existing Prometheus Integration' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:integration) { create(:prometheus_service, project: project) } + + let(:mutation) do + variables = { + id: GitlabSchema.id_from_object(integration).to_s, + api_url: 'http://modified-url.com', + active: true + } + graphql_mutation(:prometheus_integration_update, variables) do + <<~QL + clientMutationId + errors + integration { + id + active + apiUrl + } + QL + end + end + + let(:mutation_response) { graphql_mutation_response(:prometheus_integration_update) } + + before do + project.add_maintainer(user) + end + + it 'updates the integration' do + post_graphql_mutation(mutation, current_user: user) + + integration_response = mutation_response['integration'] + + expect(response).to have_gitlab_http_status(:success) + expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s) + expect(integration_response['apiUrl']).to eq('http://modified-url.com') + expect(integration_response['active']).to be_truthy + 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 ac4fa7cfe83..375d4f10b40 100644 --- a/spec/requests/api/graphql/mutations/commits/create_spec.rb +++ b/spec/requests/api/graphql/mutations/commits/create_spec.rb @@ -23,6 +23,18 @@ RSpec.describe 'Creation of a new commit' do let(:mutation) { graphql_mutation(:commit_create, input) } let(:mutation_response) { graphql_mutation_response(:commit_create) } + shared_examples 'a commit is successful' do + it 'creates a new commit' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + + expect(mutation_response['commit']).to include( + 'title' => message + ) + end + end + context 'the user is not allowed to create a commit' do it_behaves_like 'a mutation that returns a top-level access error' end @@ -32,14 +44,7 @@ RSpec.describe 'Creation of a new commit' do project.add_developer(current_user) end - it 'creates a new commit' do - post_graphql_mutation(mutation, current_user: current_user) - - expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['commit']).to include( - 'title' => message - ) - end + it_behaves_like 'a commit is successful' context 'when branch is not correct' do let(:branch) { 'unknown' } @@ -47,5 +52,22 @@ RSpec.describe 'Creation of a new commit' do it_behaves_like 'a mutation that returns errors in the response', errors: ['You can only create or edit files when you are on a branch'] end + + context 'when branch is new, and a start_branch is defined' do + let(:input) { { project_path: project.full_path, branch: branch, start_branch: start_branch, message: message, actions: actions } } + let(:branch) { 'new-branch' } + let(:start_branch) { 'master' } + let(:actions) do + [ + { + action: 'CREATE', + filePath: 'ANOTHER_FILE.md', + content: 'Bye' + } + ] + end + + it_behaves_like 'a commit is successful' + end end end diff --git a/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb b/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb index 7bef812bfec..23e8e366483 100644 --- a/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb +++ b/spec/requests/api/graphql/mutations/container_expiration_policy/update_spec.rb @@ -73,6 +73,29 @@ RSpec.describe 'Updating the container expiration policy' do end end + RSpec.shared_examples 'rejecting blank name_regex when enabled' do + context "for blank name_regex" do + let(:params) do + { + project_path: project.full_path, + name_regex: '', + enabled: true + } + end + + it_behaves_like 'returning response status', :success + + it_behaves_like 'not creating the container expiration policy' + + it 'returns an error' do + subject + + expect(graphql_data['updateContainerExpirationPolicy']['errors'].size).to eq(1) + expect(graphql_data['updateContainerExpirationPolicy']['errors']).to include("Name regex can't be blank") + end + end + end + RSpec.shared_examples 'accepting the mutation request updating the container expiration policy' do it_behaves_like 'updating the container expiration policy attributes', mode: :update, from: { cadence: '1d', keep_n: 10, older_than: '90d' }, to: { cadence: '3month', keep_n: 100, older_than: '14d' } @@ -80,6 +103,7 @@ RSpec.describe 'Updating the container expiration policy' do it_behaves_like 'rejecting invalid regex for', :name_regex it_behaves_like 'rejecting invalid regex for', :name_regex_keep + it_behaves_like 'rejecting blank name_regex when enabled' end RSpec.shared_examples 'accepting the mutation request creating the container expiration policy' do @@ -89,6 +113,7 @@ RSpec.describe 'Updating the container expiration policy' do it_behaves_like 'rejecting invalid regex for', :name_regex it_behaves_like 'rejecting invalid regex for', :name_regex_keep + it_behaves_like 'rejecting blank name_regex when enabled' end RSpec.shared_examples 'denying the mutation request' do diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb new file mode 100644 index 00000000000..645edfc2e43 --- /dev/null +++ b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Destroying a container repository' do + using RSpec::Parameterized::TableSyntax + + include GraphqlHelpers + + let_it_be_with_reload(:container_repository) { create(:container_repository) } + let_it_be(:user) { create(:user) } + + let(:project) { container_repository.project } + let(:id) { container_repository.to_global_id.to_s } + + let(:query) do + <<~GQL + containerRepository { + #{all_graphql_fields_for('ContainerRepository')} + } + errors + GQL + end + + let(:params) { { id: container_repository.to_global_id.to_s } } + let(:mutation) { graphql_mutation(:destroy_container_repository, params, query) } + let(:mutation_response) { graphql_mutation_response(:destroyContainerRepository) } + let(:container_repository_mutation_response) { mutation_response['containerRepository'] } + + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags(tags: %w[a b c]) + end + + shared_examples 'destroying the container repository' do + it 'destroy the container repository' do + expect(::Packages::CreateEventService) + .to receive(:new).with(nil, user, event_name: :delete_repository, scope: :container).and_call_original + expect(DeleteContainerRepositoryWorker) + .to receive(:perform_async).with(user.id, container_repository.id) + + expect { subject }.to change { ::Packages::Event.count }.by(1) + + expect(container_repository_mutation_response).to match_schema('graphql/container_repository') + expect(container_repository_mutation_response['status']).to eq('DELETE_SCHEDULED') + end + + it_behaves_like 'returning response status', :success + end + + shared_examples 'denying the mutation request' do + it 'does not destroy the container repository' do + expect(DeleteContainerRepositoryWorker) + .not_to receive(:perform_async).with(user.id, container_repository.id) + + expect { subject }.not_to change { ::Packages::Event.count } + + expect(mutation_response).to be_nil + end + + it_behaves_like 'returning response status', :success + end + + describe 'post graphql mutation' do + subject { post_graphql_mutation(mutation, current_user: user) } + + context 'with valid id' do + where(:user_role, :shared_examples_name) do + :maintainer | 'destroying the container repository' + :developer | 'destroying the container repository' + :reporter | 'denying the mutation request' + :guest | 'denying the mutation request' + :anonymous | 'denying the mutation request' + end + + with_them do + before do + project.send("add_#{user_role}", user) unless user_role == :anonymous + end + + it_behaves_like params[:shared_examples_name] + end + end + + context 'with invalid id' do + let(:params) { { id: 'gid://gitlab/ContainerRepository/5555' } } + + it_behaves_like 'denying the mutation request' + end + end +end diff --git a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb new file mode 100644 index 00000000000..c91437fa355 --- /dev/null +++ b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Creation of a new Custom Emoji' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group) } + + let(:attributes) do + { + name: 'my_new_emoji', + url: 'https://example.com/image.png', + group_path: group.full_path + } + end + + let(:mutation) do + graphql_mutation(:create_custom_emoji, attributes) + end + + context 'when the user has no permission' do + it 'does not create custom emoji' do + expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(CustomEmoji, :count) + end + end + + context 'when user has permission' do + before do + group.add_developer(current_user) + end + + it 'creates custom emoji' do + expect { post_graphql_mutation(mutation, current_user: current_user) }.to change(CustomEmoji, :count).by(1) + + gql_response = graphql_mutation_response(:create_custom_emoji) + expect(gql_response['errors']).to eq([]) + expect(gql_response['customEmoji']['name']).to eq(attributes[:name]) + expect(gql_response['customEmoji']['url']).to eq(attributes[:url]) + end + end +end diff --git a/spec/requests/api/graphql/mutations/labels/create_spec.rb b/spec/requests/api/graphql/mutations/labels/create_spec.rb new file mode 100644 index 00000000000..28284408306 --- /dev/null +++ b/spec/requests/api/graphql/mutations/labels/create_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Labels::Create do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + + let(:params) do + { + 'title' => 'foo', + 'description' => 'some description', + 'color' => '#FF0000' + } + end + + let(:mutation) { graphql_mutation(:label_create, params.merge(extra_params)) } + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + def mutation_response + graphql_mutation_response(:label_create) + end + + shared_examples_for 'labels create mutation' do + context 'when the user does not have permission to create a label' do + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not create the label' do + expect { subject }.not_to change { Label.count } + end + end + + context 'when the user has permission to create a label' do + before do + parent.add_developer(current_user) + end + + context 'when the parent (project_path or group_path) param is given' do + it 'creates the label' do + expect { subject }.to change { Label.count }.to(1) + + expect(mutation_response).to include( + 'label' => a_hash_including(params)) + end + + it 'does not create a label when there are errors' do + label_factory = parent.is_a?(Group) ? :group_label : :label + create(label_factory, title: 'foo', parent.class.name.underscore.to_sym => parent) + + expect { subject }.not_to change { Label.count } + + expect(mutation_response).to have_key('label') + expect(mutation_response['label']).to be_nil + expect(mutation_response['errors'].first).to eq('Title has already been taken') + end + end + end + end + + context 'when creating a project label' do + let_it_be(:parent) { create(:project) } + let(:extra_params) { { project_path: parent.full_path } } + + it_behaves_like 'labels create mutation' + end + + context 'when creating a group label' do + let_it_be(:parent) { create(:group) } + let(:extra_params) { { group_path: parent.full_path } } + + it_behaves_like 'labels create mutation' + end + + context 'when neither project_path nor group_path param is given' do + let(:mutation) { graphql_mutation(:label_create, params) } + + it_behaves_like 'a mutation that returns top-level errors', + errors: ['Exactly one of group_path or project_path arguments is required'] + + it 'does not create the label' do + expect { subject }.not_to change { Label.count } + end + end +end diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb index 81d13b29dde..2a39757e103 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,9 +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' do - let(:match_errors) { include(/is not a valid Global ID/) } - end + it_behaves_like 'an invalid argument to the mutation', argument_name: :environment_id end end end @@ -190,9 +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' do - let(:match_errors) { include(/is not a valid Global ID/) } - end + it_behaves_like 'an invalid argument to the mutation', argument_name: :cluster_id end end @@ -213,35 +209,26 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create do it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::ANNOTATION_SOURCE_ARGUMENT_ERROR] end - context 'when a non-cluster or environment id is provided' do - let(:gid) { { environment_id: project.to_global_id.to_s } } - let(:mutation) do - variables = { - starting_at: starting_at, - ending_at: ending_at, - dashboard_path: dashboard_path, - description: description - }.merge!(gid) - - graphql_mutation(:create_annotation, variables) - end - - before do - project.add_developer(current_user) - end + [:environment_id, :cluster_id].each do |arg_name| + context "when #{arg_name} is given an ID of the wrong type" do + let(:gid) { global_id_of(project) } + let(:mutation) do + variables = { + starting_at: starting_at, + ending_at: ending_at, + dashboard_path: dashboard_path, + description: description, + arg_name => gid + } - describe 'non-environment id' do - it_behaves_like 'a mutation that returns top-level errors' do - let(:match_errors) { include(/does not represent an instance of Environment/) } + graphql_mutation(:create_annotation, variables) end - end - - describe 'non-cluster id' do - let(:gid) { { cluster_id: project.to_global_id.to_s } } - it_behaves_like 'a mutation that returns top-level errors' do - let(:match_errors) { include(/does not represent an instance of Clusters::Cluster/) } + before do + project.add_developer(current_user) end + + it_behaves_like 'an invalid argument to the mutation', argument_name: arg_name end end end diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb index 9a612c841a2..b956734068c 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 @@ -36,7 +36,7 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete do let(:variables) { { id: GitlabSchema.id_from_object(project).to_s } } it_behaves_like 'a mutation that returns top-level errors' do - let(:match_errors) { eq(["#{variables[:id]} is not a valid ID for #{annotation.class}."]) } + let(:match_errors) { contain_exactly(include('invalid value for id')) } end end diff --git a/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb new file mode 100644 index 00000000000..4efa7f9d509 --- /dev/null +++ b/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Repositioning an ImageDiffNote' do + include GraphqlHelpers + + let_it_be(:noteable) { create(:merge_request) } + let_it_be(:project) { noteable.project } + let(:note) { create(:image_diff_note_on_merge_request, noteable: noteable, project: project) } + let(:new_position) { { x: 10 } } + let(:current_user) { project.creator } + + let(:mutation_variables) do + { + id: global_id_of(note), + position: new_position + } + end + + let(:mutation) do + graphql_mutation(:reposition_image_diff_note, mutation_variables) do + <<~QL + note { + id + } + errors + QL + end + end + + def mutation_response + graphql_mutation_response(:reposition_image_diff_note) + end + + it 'updates the note', :aggregate_failures do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change { note.reset.position.x }.to(10) + + expect(mutation_response['note']).to eq('id' => global_id_of(note)) + expect(mutation_response['errors']).to be_empty + end + + context 'when the note is not a DiffNote' do + let(:note) { project } + + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/does not represent an instance of DiffNote/) } + end + end + + context 'when a position arg is nil' do + let(:new_position) { { x: nil, y: 10 } } + + it 'does not set the property to nil', :aggregate_failures do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.not_to change { note.reset.position.x } + + expect(mutation_response['note']).to eq('id' => global_id_of(note)) + expect(mutation_response['errors']).to be_empty + end + end + + context 'when all position args are nil' do + let(:new_position) { { x: nil } } + + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { include(/RepositionImageDiffNoteInput! was provided invalid value/) } + end + + it 'contains an explanation for the error' do + post_graphql_mutation(mutation, current_user: current_user) + + explanation = graphql_errors.first['extensions']['problems'].first['explanation'] + + expect(explanation).to eq('At least one property of `UpdateDiffImagePositionInput` must be set') + end + end +end 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 efa2ceb65c2..713b26a6a9b 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 @@ -20,6 +20,7 @@ RSpec.describe 'Updating an image DiffNote' do position_type: 'image' ) end + let_it_be(:updated_body) { 'Updated body' } let_it_be(:updated_width) { 50 } let_it_be(:updated_height) { 100 } @@ -31,7 +32,7 @@ RSpec.describe 'Updating an image DiffNote' do height: updated_height, x: updated_x, y: updated_y - } + }.compact.presence end let!(:diff_note) do @@ -45,10 +46,11 @@ RSpec.describe 'Updating an image DiffNote' do let(:mutation) do variables = { id: GitlabSchema.id_from_object(diff_note).to_s, - body: updated_body, - position: updated_position + body: updated_body } + variables[:position] = updated_position if updated_position + graphql_mutation(:update_image_diff_note, variables) end diff --git a/spec/requests/api/graphql/mutations/releases/create_spec.rb b/spec/requests/api/graphql/mutations/releases/create_spec.rb new file mode 100644 index 00000000000..d745eb3083d --- /dev/null +++ b/spec/requests/api/graphql/mutations/releases/create_spec.rb @@ -0,0 +1,375 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Creation of a new release' do + include GraphqlHelpers + include Presentable + + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:milestone_12_3) { create(:milestone, project: project, title: '12.3') } + let_it_be(:milestone_12_4) { create(:milestone, project: project, title: '12.4') } + let_it_be(:public_user) { create(:user) } + let_it_be(:guest) { create(:user) } + let_it_be(:reporter) { create(:user) } + let_it_be(:developer) { create(:user) } + + let(:mutation_name) { :release_create } + + let(:tag_name) { 'v7.12.5'} + let(:ref) { 'master'} + let(:name) { 'Version 7.12.5'} + let(:description) { 'Release 7.12.5 :rocket:' } + let(:released_at) { '2018-12-10' } + let(:milestones) { [milestone_12_3.title, milestone_12_4.title] } + let(:asset_link) { { name: 'An asset link', url: 'https://gitlab.example.com/link', directAssetPath: '/permanent/link', linkType: 'OTHER' } } + let(:assets) { { links: [asset_link] } } + + let(:mutation_arguments) do + { + projectPath: project.full_path, + tagName: tag_name, + ref: ref, + name: name, + description: description, + releasedAt: released_at, + milestones: milestones, + assets: assets + } + end + + let(:mutation) do + graphql_mutation(mutation_name, mutation_arguments, <<~FIELDS) + release { + tagName + name + description + releasedAt + createdAt + milestones { + nodes { + title + } + } + assets { + links { + nodes { + name + url + linkType + external + directAssetUrl + } + } + } + } + errors + FIELDS + end + + let(:create_release) { post_graphql_mutation(mutation, current_user: current_user) } + let(:mutation_response) { graphql_mutation_response(mutation_name)&.with_indifferent_access } + + around do |example| + freeze_time { example.run } + end + + before do + project.add_guest(guest) + project.add_reporter(reporter) + project.add_developer(developer) + + stub_default_url_options(host: 'www.example.com') + end + + shared_examples 'no errors' do + it 'returns no errors' do + create_release + + expect(graphql_errors).not_to be_present + end + end + + shared_examples 'top-level error with message' do |error_message| + it 'returns a top-level error with message' do + create_release + + expect(mutation_response).to be_nil + expect(graphql_errors.count).to eq(1) + expect(graphql_errors.first['message']).to eq(error_message) + end + end + + shared_examples 'errors-as-data with message' do |error_message| + it 'returns an error-as-data with message' do + create_release + + expect(mutation_response[:release]).to be_nil + expect(mutation_response[:errors].count).to eq(1) + expect(mutation_response[:errors].first).to match(error_message) + end + end + + context 'when the current user has access to create releases' do + let(:current_user) { developer } + + context 'when all available mutation arguments are provided' do + it_behaves_like 'no errors' + + # rubocop: disable CodeReuse/ActiveRecord + it 'returns the new release data' do + create_release + + release = mutation_response[:release] + expected_direct_asset_url = Gitlab::Routing.url_helpers.project_release_url(project, Release.find_by(tag: tag_name)) << "/downloads#{asset_link[:directAssetPath]}" + + expected_attributes = { + tagName: tag_name, + name: name, + description: description, + releasedAt: Time.parse(released_at).utc.iso8601, + createdAt: Time.current.utc.iso8601, + assets: { + links: { + nodes: [{ + name: asset_link[:name], + url: asset_link[:url], + linkType: asset_link[:linkType], + external: true, + directAssetUrl: expected_direct_asset_url + }] + } + } + } + + expect(release).to include(expected_attributes) + + # Right now the milestones are returned in a non-deterministic order. + # This `milestones` test should be moved up into the expect(release) + # above (and `.to include` updated to `.to eq`) once + # https://gitlab.com/gitlab-org/gitlab/-/issues/259012 is addressed. + expect(release['milestones']['nodes']).to match_array([ + { 'title' => '12.4' }, + { 'title' => '12.3' } + ]) + end + # rubocop: enable CodeReuse/ActiveRecord + end + + context 'when only the required mutation arguments are provided' do + let(:mutation_arguments) { super().slice(:projectPath, :tagName, :ref) } + + it_behaves_like 'no errors' + + it 'returns the new release data' do + create_release + + expected_response = { + tagName: tag_name, + name: tag_name, + description: nil, + releasedAt: Time.current.utc.iso8601, + createdAt: Time.current.utc.iso8601, + milestones: { + nodes: [] + }, + assets: { + links: { + nodes: [] + } + } + }.with_indifferent_access + + expect(mutation_response[:release]).to eq(expected_response) + end + end + + context 'when the provided tag already exists' do + let(:tag_name) { 'v1.1.0' } + + it_behaves_like 'no errors' + + it 'does not create a new tag' do + expect { create_release }.not_to change { Project.find_by_id(project.id).repository.tag_count } + end + end + + context 'when the provided tag does not already exist' do + let(:tag_name) { 'v7.12.5-alpha' } + + it_behaves_like 'no errors' + + it 'creates a new tag' do + expect { create_release }.to change { Project.find_by_id(project.id).repository.tag_count }.by(1) + end + end + + context 'when a local timezone is provided for releasedAt' do + let(:released_at) { Time.parse(super()).in_time_zone('Hawaii').iso8601 } + + it_behaves_like 'no errors' + + it 'returns the correct releasedAt date in UTC' do + create_release + + expect(mutation_response[:release]).to include({ releasedAt: Time.parse(released_at).utc.iso8601 }) + end + end + + context 'when no releasedAt is provided' do + let(:mutation_arguments) { super().except(:releasedAt) } + + it_behaves_like 'no errors' + + it 'sets releasedAt to the current time' do + create_release + + expect(mutation_response[:release]).to include({ releasedAt: Time.current.utc.iso8601 }) + end + end + + context "when a release asset doesn't include an explicit linkType" do + let(:asset_link) { super().except(:linkType) } + + it_behaves_like 'no errors' + + it 'defaults the linkType to OTHER' do + create_release + + returned_asset_link_type = mutation_response.dig(:release, :assets, :links, :nodes, 0, :linkType) + + expect(returned_asset_link_type).to eq('OTHER') + end + end + + context "when a release asset doesn't include a directAssetPath" do + let(:asset_link) { super().except(:directAssetPath) } + + it_behaves_like 'no errors' + + it 'returns the provided url as the directAssetUrl' do + create_release + + returned_asset_link_type = mutation_response.dig(:release, :assets, :links, :nodes, 0, :directAssetUrl) + + expect(returned_asset_link_type).to eq(asset_link[:url]) + end + end + + context 'empty milestones' do + shared_examples 'no associated milestones' do + it_behaves_like 'no errors' + + it 'creates a release with no associated milestones' do + create_release + + returned_milestones = mutation_response.dig(:release, :milestones, :nodes) + + expect(returned_milestones.count).to eq(0) + end + end + + context 'when the milestones parameter is not provided' do + let(:mutation_arguments) { super().except(:milestones) } + + it_behaves_like 'no associated milestones' + end + + context 'when the milestones parameter is null' do + let(:milestones) { nil } + + it_behaves_like 'no associated milestones' + end + + context 'when the milestones parameter is an empty array' do + let(:milestones) { [] } + + it_behaves_like 'no associated milestones' + end + end + + context 'validation' do + context 'when a release is already associated to the specified tag' do + before do + create(:release, project: project, tag: tag_name) + end + + it_behaves_like 'errors-as-data with message', 'Release already exists' + end + + context "when a provided milestone doesn\'t exist" do + let(:milestones) { ['a fake milestone'] } + + it_behaves_like 'errors-as-data with message', 'Milestone(s) not found: a fake milestone' + end + + context "when a provided milestone belongs to a different project than the release" do + let(:milestone_in_different_project) { create(:milestone, title: 'different milestone') } + let(:milestones) { [milestone_in_different_project.title] } + + it_behaves_like 'errors-as-data with message', "Milestone(s) not found: different milestone" + end + + context 'when two release assets share the same name' do + let(:asset_link_1) { { name: 'My link', url: 'https://example.com/1' } } + let(:asset_link_2) { { name: 'My link', url: 'https://example.com/2' } } + let(:assets) { { links: [asset_link_1, asset_link_2] } } + + # Right now the raw Postgres error message is sent to the user as the validation message. + # We should catch this validation error and return a nicer message: + # https://gitlab.com/gitlab-org/gitlab/-/issues/277087 + it_behaves_like 'errors-as-data with message', 'PG::UniqueViolation' + end + + context 'when two release assets share the same URL' do + let(:asset_link_1) { { name: 'My first link', url: 'https://example.com' } } + let(:asset_link_2) { { name: 'My second link', url: 'https://example.com' } } + let(:assets) { { links: [asset_link_1, asset_link_2] } } + + # Same note as above about the ugly error message + it_behaves_like 'errors-as-data with message', 'PG::UniqueViolation' + end + + context 'when the provided tag name is HEAD' do + let(:tag_name) { 'HEAD' } + + it_behaves_like 'errors-as-data with message', 'Tag name invalid' + end + + context 'when the provided tag name is empty' do + let(:tag_name) { '' } + + it_behaves_like 'errors-as-data with message', 'Tag name invalid' + end + + context "when the provided tag doesn't already exist, and no ref parameter was provided" do + let(:ref) { nil } + let(:tag_name) { 'v7.12.5-beta' } + + it_behaves_like 'errors-as-data with message', 'Ref is not specified' + end + end + end + + context "when the current user doesn't have access to create releases" do + expected_error_message = "The resource that you are attempting to access does not exist or you don't have permission to perform this action" + + context 'when the current user is a Reporter' do + let(:current_user) { reporter } + + it_behaves_like 'top-level error with message', expected_error_message + end + + context 'when the current user is a Guest' do + let(:current_user) { guest } + + it_behaves_like 'top-level error with message', expected_error_message + end + + context 'when the current user is a public user' do + let(:current_user) { public_user } + + it_behaves_like 'top-level error with message', expected_error_message + end + end +end diff --git a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb index b71f87d2702..1be8ce142ac 100644 --- a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb @@ -53,10 +53,11 @@ RSpec.describe 'Destroying a Snippet' do let!(:snippet_gid) { project.to_gid.to_s } it 'returns an error' do + err_message = %Q["#{snippet_gid}" does not represent an instance of Snippet] + 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.")) + expect(graphql_errors).to include(a_hash_including('message' => a_string_including(err_message))) end it 'does not destroy the Snippet' do diff --git a/spec/requests/api/graphql/mutations/todos/create_spec.rb b/spec/requests/api/graphql/mutations/todos/create_spec.rb new file mode 100644 index 00000000000..aca00519682 --- /dev/null +++ b/spec/requests/api/graphql/mutations/todos/create_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Create a todo' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:target) { create(:issue) } + + let(:input) do + { + 'targetId' => target.to_global_id.to_s + } + end + + let(:mutation) { graphql_mutation(:todoCreate, input) } + + let(:mutation_response) { graphql_mutation_response(:todoCreate) } + + context 'the user is not allowed to create todo' do + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user has permissions to create todo' do + before do + target.project.add_guest(current_user) + end + + it 'creates todo' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['todo']['body']).to eq(target.title) + expect(mutation_response['todo']['state']).to eq('pending') + end + end +end diff --git a/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb new file mode 100644 index 00000000000..3e96d5c5058 --- /dev/null +++ b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Restoring many Todos' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:author) { create(:user) } + let_it_be(:other_user) { create(:user) } + + let_it_be(:todo1) { create(:todo, user: current_user, author: author, state: :done) } + let_it_be(:todo2) { create(:todo, user: current_user, author: author, state: :done) } + + let_it_be(:other_user_todo) { create(:todo, user: other_user, author: author, state: :done) } + + let(:input_ids) { [todo1, todo2].map { |obj| global_id_of(obj) } } + let(:input) { { ids: input_ids } } + + let(:mutation) do + graphql_mutation(:todo_restore_many, input, + <<-QL.strip_heredoc + clientMutationId + errors + updatedIds + todos { + id + state + } + QL + ) + end + + def mutation_response + graphql_mutation_response(:todo_restore_many) + end + + it 'restores many todos' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(todo1.reload.state).to eq('pending') + expect(todo2.reload.state).to eq('pending') + expect(other_user_todo.reload.state).to eq('done') + + expect(mutation_response).to include( + 'errors' => be_empty, + 'updatedIds' => match_array(input_ids), + 'todos' => contain_exactly( + { 'id' => global_id_of(todo1), 'state' => 'pending' }, + { 'id' => global_id_of(todo2), 'state' => 'pending' } + ) + ) + end + + context 'when using an invalid gid' do + let(:input_ids) { [global_id_of(author)] } + let(:invalid_gid_error) { /does not represent an instance of #{todo1.class}/ } + + it 'contains the expected error' do + post_graphql_mutation(mutation, current_user: current_user) + + errors = json_response['errors'] + expect(errors).not_to be_blank + expect(errors.first['message']).to match(invalid_gid_error) + + expect(todo1.reload.state).to eq('done') + expect(todo2.reload.state).to eq('done') + end + end +end diff --git a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb index 44e68c59248..37cc502103d 100644 --- a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb +++ b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb @@ -6,7 +6,7 @@ RSpec.describe 'rendering namespace statistics' do include GraphqlHelpers let(:namespace) { user.namespace } - let!(:statistics) { create(:namespace_root_storage_statistics, namespace: namespace, packages_size: 5.gigabytes) } + let!(:statistics) { create(:namespace_root_storage_statistics, namespace: namespace, packages_size: 5.gigabytes, uploads_size: 3.gigabytes) } let(:user) { create(:user) } let(:query) do @@ -28,6 +28,12 @@ RSpec.describe 'rendering namespace statistics' do expect(graphql_data['namespace']['rootStorageStatistics']).not_to be_blank expect(graphql_data['namespace']['rootStorageStatistics']['packagesSize']).to eq(5.gigabytes) end + + it 'includes uploads size if the user can read the statistics' do + post_graphql(query, current_user: user) + + expect(graphql_data_at(:namespace, :root_storage_statistics, :uploads_size)).to eq(3.gigabytes) + end end it_behaves_like 'a working namespace with storage statistics query' diff --git a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb new file mode 100644 index 00000000000..b13805a61ce --- /dev/null +++ b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'getting Alert Management Integrations' do + include ::Gitlab::Routing + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:current_user) { create(:user) } + let_it_be(:prometheus_service) { create(:prometheus_service, project: project) } + let_it_be(:project_alerting_setting) { create(:project_alerting_setting, project: project) } + let_it_be(:active_http_integration) { create(:alert_management_http_integration, project: project) } + let_it_be(:inactive_http_integration) { create(:alert_management_http_integration, :inactive, project: project) } + let_it_be(:other_project_http_integration) { create(:alert_management_http_integration) } + + let(:fields) do + <<~QUERY + nodes { + #{all_graphql_fields_for('AlertManagementIntegration')} + } + QUERY + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('alertManagementIntegrations', {}, fields) + ) + end + + context 'with integrations' do + let(:integrations) { graphql_data.dig('project', 'alertManagementIntegrations', 'nodes') } + + context 'without project permissions' do + let(:user) { create(:user) } + + before do + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + + it { expect(integrations).to be_nil } + end + + context 'with project permissions' do + before do + project.add_maintainer(current_user) + post_graphql(query, current_user: current_user) + end + + let(:http_integration) { integrations.first } + let(:prometheus_integration) { integrations.second } + + it_behaves_like 'a working graphql query' + + it { expect(integrations.size).to eq(2) } + + it 'returns the correct properties of the integrations' do + expect(http_integration).to include( + 'id' => GitlabSchema.id_from_object(active_http_integration).to_s, + 'type' => 'HTTP', + 'name' => active_http_integration.name, + 'active' => active_http_integration.active, + 'token' => active_http_integration.token, + 'url' => active_http_integration.url, + 'apiUrl' => nil + ) + + expect(prometheus_integration).to include( + 'id' => GitlabSchema.id_from_object(prometheus_service).to_s, + 'type' => 'PROMETHEUS', + 'name' => 'Prometheus', + 'active' => prometheus_service.manual_configuration?, + 'token' => project_alerting_setting.token, + 'url' => "http://localhost/#{project.full_path}/prometheus/alerts/notify.json", + 'apiUrl' => prometheus_service.api_url + ) + end + end + end +end diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb new file mode 100644 index 00000000000..7e32f54bf1d --- /dev/null +++ b/spec/requests/api/graphql/project/container_repositories_spec.rb @@ -0,0 +1,145 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'getting container repositories in a project' do + using RSpec::Parameterized::TableSyntax + include GraphqlHelpers + + let_it_be_with_reload(:project) { create(:project, :private) } + let_it_be(:container_repository) { create(:container_repository, project: project) } + let_it_be(:container_repositories_delete_scheduled) { create_list(:container_repository, 2, :status_delete_scheduled, project: project) } + let_it_be(:container_repositories_delete_failed) { create_list(:container_repository, 2, :status_delete_failed, project: project) } + let_it_be(:container_repositories) { [container_repository, container_repositories_delete_scheduled, container_repositories_delete_failed].flatten } + let_it_be(:container_expiration_policy) { project.container_expiration_policy } + + let(:fields) do + <<~GQL + edges { + node { + #{all_graphql_fields_for('container_repositories'.classify)} + } + } + GQL + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('container_repositories', {}, fields) + ) + end + + let(:user) { project.owner } + let(:variables) { {} } + let(:container_repositories_response) { graphql_data.dig('project', 'containerRepositories', 'edges') } + + before do + stub_container_registry_config(enabled: true) + container_repositories.each do |repository| + stub_container_registry_tags(repository: repository.path, tags: %w(tag1 tag2 tag3), with_manifest: false) + end + end + + subject { post_graphql(query, current_user: user, variables: variables) } + + it_behaves_like 'a working graphql query' do + before do + subject + end + + it 'matches the JSON schema' do + expect(container_repositories_response).to match_schema('graphql/container_repositories') + end + end + + context 'with different permissions' do + let_it_be(:user) { create(:user) } + + where(:project_visibility, :role, :access_granted, :can_delete) do + :private | :maintainer | true | true + :private | :developer | true | true + :private | :reporter | true | false + :private | :guest | false | false + :private | :anonymous | false | false + :public | :maintainer | true | true + :public | :developer | true | true + :public | :reporter | true | false + :public | :guest | true | false + :public | :anonymous | true | false + end + + with_them do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility.to_s.upcase, false)) + project.add_user(user, role) unless role == :anonymous + end + + it 'return the proper response' do + subject + + if access_granted + expect(container_repositories_response.size).to eq(container_repositories.size) + container_repositories_response.each do |repository_response| + expect(repository_response.dig('node', 'canDelete')).to eq(can_delete) + end + else + expect(container_repositories_response).to eq(nil) + end + end + end + end + + context 'limiting the number of repositories' do + let(:limit) { 1 } + let(:variables) do + { path: project.full_path, n: limit } + end + + let(:query) do + <<~GQL + query($path: ID!, $n: Int) { + project(fullPath: $path) { + containerRepositories(first: $n) { #{fields} } + } + } + GQL + end + + it 'only returns N repositories' do + subject + + expect(container_repositories_response.size).to eq(limit) + end + end + + context 'filter by name' do + let_it_be(:container_repository) { create(:container_repository, name: 'fooBar', project: project) } + + let(:name) { 'ooba' } + let(:query) do + <<~GQL + query($path: ID!, $name: String) { + project(fullPath: $path) { + containerRepositories(name: $name) { #{fields} } + } + } + GQL + end + + let(:variables) do + { path: project.full_path, name: name } + end + + before do + stub_container_registry_tags(repository: container_repository.path, tags: %w(tag4 tag5 tag6), with_manifest: false) + end + + it 'returns the searched container repository' do + subject + + expect(container_repositories_response.size).to eq(1) + expect(container_repositories_response.first.dig('node', 'id')).to eq(container_repository.to_global_id.to_s) + end + end +end diff --git a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb index cd84ce9cb96..c7d327a62af 100644 --- a/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb +++ b/spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb @@ -29,10 +29,12 @@ RSpec.describe 'sentry errors requests' do let(:error_data) { graphql_data.dig('project', 'sentryErrors', 'detailedError') } - it_behaves_like 'a working graphql query' do - before do - post_graphql(query, current_user: current_user) - end + it 'returns a successful response', :aggregate_failures, :quarantine do + post_graphql(query, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(graphql_errors).to be_nil + expect(json_response.keys).to include('data') end context 'when data is loading via reactive cache' do @@ -191,7 +193,7 @@ RSpec.describe 'sentry errors requests' do describe 'getting a stack trace' do let_it_be(:sentry_stack_trace) { build(:error_tracking_error_event) } - let(:sentry_gid) { Gitlab::ErrorTracking::DetailedError.new(id: 1).to_global_id.to_s } + let(:sentry_gid) { global_id_of(Gitlab::ErrorTracking::DetailedError.new(id: 1)) } let(:stack_trace_fields) do all_graphql_fields_for('SentryErrorStackTrace'.classify) diff --git a/spec/requests/api/graphql/project/grafana_integration_spec.rb b/spec/requests/api/graphql/project/grafana_integration_spec.rb index 688959e622d..9b24698f40c 100644 --- a/spec/requests/api/graphql/project/grafana_integration_spec.rb +++ b/spec/requests/api/graphql/project/grafana_integration_spec.rb @@ -45,7 +45,6 @@ RSpec.describe 'Getting Grafana Integration' do it_behaves_like 'a working graphql query' - specify { expect(integration_data['token']).to eql grafana_integration.masked_token } specify { expect(integration_data['grafanaUrl']).to eql grafana_integration.grafana_url } specify do diff --git a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb index 1b654e660e3..4bce3c7fe0f 100644 --- a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb +++ b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb @@ -14,6 +14,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha) create(:design_version, issue: issue, created_designs: create_list(:design, 3, issue: issue)) end + let_it_be(:version) do create(:design_version, issue: issue, modified_designs: old_version.designs, diff --git a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb index 640ac95cd86..ee0085718b3 100644 --- a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb +++ b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb @@ -11,12 +11,15 @@ RSpec.describe 'Getting versions related to an issue' do let_it_be(:version_a) do create(:design_version, issue: issue) end + let_it_be(:version_b) do create(:design_version, issue: issue) end + let_it_be(:version_c) do create(:design_version, issue: issue) end + let_it_be(:version_d) do create(:design_version, issue: issue) 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 e25453510d5..a671ddc7ab1 100644 --- a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb +++ b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb @@ -30,7 +30,7 @@ RSpec.describe 'Getting designs related to an issue' do post_graphql(query(note_fields), current_user: nil) - designs_data = graphql_data['project']['issue']['designs']['designs'] + designs_data = graphql_data['project']['issue']['designCollection']['designs'] design_data = designs_data['nodes'].first note_data = design_data['notes']['nodes'].first @@ -56,7 +56,7 @@ RSpec.describe 'Getting designs related to an issue' do 'issue', { iid: design.issue.iid.to_s }, query_graphql_field( - 'designs', {}, design_node + 'designCollection', {}, design_node ) ) ) diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 40fec6ba068..4f27f08bf98 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -9,10 +9,9 @@ RSpec.describe 'getting an issue list for a project' 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, :with_alert, project: project)] - end + let_it_be(:issue_a, reload: true) { create(:issue, project: project, discussion_locked: true) } + let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project) } + let_it_be(:issues, reload: true) { [issue_a, issue_b] } let(:fields) do <<~QUERY @@ -414,4 +413,42 @@ RSpec.describe 'getting an issue list for a project' do expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(new_issues)) end end + + describe 'N+1 query checks' do + let(:extra_iid_for_second_query) { issue_b.iid.to_s } + let(:search_params) { { iids: [issue_a.iid.to_s] } } + + def execute_query + query = graphql_query_for( + :project, + { full_path: project.full_path }, + query_graphql_field(:issues, search_params, [ + query_graphql_field(:nodes, nil, requested_fields) + ]) + ) + post_graphql(query, current_user: current_user) + end + + context 'when requesting `user_notes_count`' do + let(:requested_fields) { [:user_notes_count] } + + before do + create_list(:note_on_issue, 2, noteable: issue_a, project: project) + create(:note_on_issue, noteable: issue_b, project: project) + end + + include_examples 'N+1 query check' + end + + context 'when requesting `user_discussions_count`' do + let(:requested_fields) { [:user_discussions_count] } + + before do + create_list(:note_on_issue, 2, noteable: issue_a, project: project) + create(:note_on_issue, noteable: issue_b, project: project) + end + + include_examples 'N+1 query check' + end + end end diff --git a/spec/requests/api/graphql/project/jira_import_spec.rb b/spec/requests/api/graphql/project/jira_import_spec.rb index 1cc30b95162..98a3f08baa6 100644 --- a/spec/requests/api/graphql/project/jira_import_spec.rb +++ b/spec/requests/api/graphql/project/jira_import_spec.rb @@ -19,6 +19,7 @@ RSpec.describe 'query Jira import data' do total_issue_count: 4 ) end + let_it_be(:jira_import2) do create( :jira_import_state, :finished, @@ -31,6 +32,7 @@ RSpec.describe 'query Jira import data' do total_issue_count: 3 ) end + let(:query) do %( query { diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb index c737e0b8caf..2b8d537f9fc 100644 --- a/spec/requests/api/graphql/project/merge_requests_spec.rb +++ b/spec/requests/api/graphql/project/merge_requests_spec.rb @@ -243,6 +243,17 @@ RSpec.describe 'getting merge request listings nested in a project' do include_examples 'N+1 query check' end + + context 'when requesting `user_discussions_count`' do + let(:requested_fields) { [:user_discussions_count] } + + before do + create_list(:note_on_merge_request, 2, noteable: merge_request_a, project: project) + create(:note_on_merge_request, noteable: merge_request_c, project: project) + end + + include_examples 'N+1 query check' + end end describe 'sorting and pagination' do diff --git a/spec/requests/api/graphql/project/project_statistics_spec.rb b/spec/requests/api/graphql/project/project_statistics_spec.rb index c226b10ab51..b57c594c64f 100644 --- a/spec/requests/api/graphql/project/project_statistics_spec.rb +++ b/spec/requests/api/graphql/project/project_statistics_spec.rb @@ -6,7 +6,7 @@ RSpec.describe 'rendering project statistics' do include GraphqlHelpers let(:project) { create(:project) } - let!(:project_statistics) { create(:project_statistics, project: project, packages_size: 5.gigabytes) } + let!(:project_statistics) { create(:project_statistics, project: project, packages_size: 5.gigabytes, uploads_size: 3.gigabytes) } let(:user) { create(:user) } let(:query) do @@ -31,6 +31,12 @@ RSpec.describe 'rendering project statistics' do expect(graphql_data['project']['statistics']['packagesSize']).to eq(5.gigabytes) end + it 'includes uploads size if the user can read the statistics' do + post_graphql(query, current_user: user) + + expect(graphql_data_at(:project, :statistics, :uploadsSize)).to eq(3.gigabytes) + end + context 'when the project is public' do let(:project) { create(:project, :public) } diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb index 8fce29d0dc6..57dbe258ce4 100644 --- a/spec/requests/api/graphql/project/release_spec.rb +++ b/spec/requests/api/graphql/project/release_spec.rb @@ -13,7 +13,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do 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(:base_url_params) { { scope: 'all', release_tag: release.tag } } + let(:opened_url_params) { { state: 'opened', **base_url_params } } + let(:merged_url_params) { { state: 'merged', **base_url_params } } + let(:closed_url_params) { { state: 'closed', **base_url_params } } + let(:post_query) { post_graphql(query, current_user: current_user) } let(:path_prefix) { %w[project release] } let(:data) { graphql_data.dig(*path) } @@ -143,7 +147,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do 'name' => link.name, 'url' => link.url, 'external' => link.external?, - 'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << link.filepath : link.url + 'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url } end @@ -180,8 +184,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do let(:release_fields) do query_graphql_field(:links, nil, %{ selfUrl - mergeRequestsUrl - issuesUrl + openedMergeRequestsUrl + mergedMergeRequestsUrl + closedMergeRequestsUrl + openedIssuesUrl + closedIssuesUrl }) end @@ -190,8 +197,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do expect(data).to eq( 'selfUrl' => project_release_url(project, release), - 'mergeRequestsUrl' => project_merge_requests_url(project, params_for_issues_and_mrs), - 'issuesUrl' => project_issues_url(project, params_for_issues_and_mrs) + 'openedMergeRequestsUrl' => project_merge_requests_url(project, opened_url_params), + 'mergedMergeRequestsUrl' => project_merge_requests_url(project, merged_url_params), + 'closedMergeRequestsUrl' => project_merge_requests_url(project, closed_url_params), + 'openedIssuesUrl' => project_issues_url(project, opened_url_params), + 'closedIssuesUrl' => project_issues_url(project, closed_url_params) ) end end diff --git a/spec/requests/api/graphql/project/releases_spec.rb b/spec/requests/api/graphql/project/releases_spec.rb index 7c57c0e9177..6e364c7d7b5 100644 --- a/spec/requests/api/graphql/project/releases_spec.rb +++ b/spec/requests/api/graphql/project/releases_spec.rb @@ -10,6 +10,11 @@ RSpec.describe 'Query.project(fullPath).releases()' do let_it_be(:reporter) { create(:user) } let_it_be(:developer) { create(:user) } + let(:base_url_params) { { scope: 'all', release_tag: release.tag } } + let(:opened_url_params) { { state: 'opened', **base_url_params } } + let(:merged_url_params) { { state: 'merged', **base_url_params } } + let(:closed_url_params) { { state: 'closed', **base_url_params } } + let(:query) do graphql_query_for(:project, { fullPath: project.full_path }, %{ @@ -37,8 +42,11 @@ RSpec.describe 'Query.project(fullPath).releases()' do } links { selfUrl - mergeRequestsUrl - issuesUrl + openedMergeRequestsUrl + mergedMergeRequestsUrl + closedMergeRequestsUrl + openedIssuesUrl + closedIssuesUrl } } } @@ -101,8 +109,11 @@ RSpec.describe 'Query.project(fullPath).releases()' do }, 'links' => { 'selfUrl' => project_release_url(project, release), - 'mergeRequestsUrl' => project_merge_requests_url(project, params_for_issues_and_mrs), - 'issuesUrl' => project_issues_url(project, params_for_issues_and_mrs) + 'openedMergeRequestsUrl' => project_merge_requests_url(project, opened_url_params), + 'mergedMergeRequestsUrl' => project_merge_requests_url(project, merged_url_params), + 'closedMergeRequestsUrl' => project_merge_requests_url(project, closed_url_params), + 'openedIssuesUrl' => project_issues_url(project, opened_url_params), + 'closedIssuesUrl' => project_issues_url(project, closed_url_params) } ) end @@ -300,4 +311,77 @@ RSpec.describe 'Query.project(fullPath).releases()' do it_behaves_like 'no access to any release data' end end + + describe 'sorting behavior' do + let_it_be(:today) { Time.now } + let_it_be(:yesterday) { today - 1.day } + let_it_be(:tomorrow) { today + 1.day } + + let_it_be(:project) { create(:project, :repository, :public) } + + let_it_be(:release_v1) { create(:release, project: project, tag: 'v1', released_at: yesterday, created_at: tomorrow) } + let_it_be(:release_v2) { create(:release, project: project, tag: 'v2', released_at: today, created_at: yesterday) } + let_it_be(:release_v3) { create(:release, project: project, tag: 'v3', released_at: tomorrow, created_at: today) } + + let(:current_user) { developer } + + let(:params) { nil } + + let(:sorted_tags) do + graphql_data.dig('project', 'releases', 'nodes').map { |release| release['tagName'] } + end + + let(:query) do + graphql_query_for(:project, { fullPath: project.full_path }, + %{ + releases#{params ? "(#{params})" : ""} { + nodes { + tagName + } + } + }) + end + + before do + post_query + end + + context 'when no sort: parameter is provided' do + it 'returns the results with the default sort applied (sort: RELEASED_AT_DESC)' do + expect(sorted_tags).to eq(%w(v3 v2 v1)) + end + end + + context 'with sort: RELEASED_AT_DESC' do + let(:params) { 'sort: RELEASED_AT_DESC' } + + it 'returns the releases ordered by released_at in descending order' do + expect(sorted_tags).to eq(%w(v3 v2 v1)) + end + end + + context 'with sort: RELEASED_AT_ASC' do + let(:params) { 'sort: RELEASED_AT_ASC' } + + it 'returns the releases ordered by released_at in ascending order' do + expect(sorted_tags).to eq(%w(v1 v2 v3)) + end + end + + context 'with sort: CREATED_DESC' do + let(:params) { 'sort: CREATED_DESC' } + + it 'returns the releases ordered by created_at in descending order' do + expect(sorted_tags).to eq(%w(v1 v3 v2)) + end + end + + context 'with sort: CREATED_ASC' do + let(:params) { 'sort: CREATED_ASC' } + + it 'returns the releases ordered by created_at in ascending order' do + expect(sorted_tags).to eq(%w(v2 v3 v1)) + end + end + end end diff --git a/spec/requests/api/graphql/project/terraform/states_spec.rb b/spec/requests/api/graphql/project/terraform/states_spec.rb new file mode 100644 index 00000000000..8b67b549efa --- /dev/null +++ b/spec/requests/api/graphql/project/terraform/states_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'query terraform states' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:terraform_state) { create(:terraform_state, :with_version, :locked, project: project) } + let_it_be(:latest_version) { terraform_state.latest_version } + + let(:query) do + graphql_query_for(:project, { fullPath: project.full_path }, + %{ + terraformStates { + count + nodes { + id + name + lockedAt + createdAt + updatedAt + + latestVersion { + id + createdAt + updatedAt + + createdByUser { + id + } + + job { + name + } + } + + lockedByUser { + id + } + } + } + }) + end + + let(:current_user) { project.creator } + let(:data) { graphql_data.dig('project', 'terraformStates') } + + before do + post_graphql(query, current_user: current_user) + end + + it 'returns terraform state data', :aggregate_failures do + state = data.dig('nodes', 0) + version = state['latestVersion'] + + expect(state['id']).to eq(terraform_state.to_global_id.to_s) + expect(state['name']).to eq(terraform_state.name) + expect(state['lockedAt']).to eq(terraform_state.locked_at.iso8601) + expect(state['createdAt']).to eq(terraform_state.created_at.iso8601) + expect(state['updatedAt']).to eq(terraform_state.updated_at.iso8601) + expect(state.dig('lockedByUser', 'id')).to eq(terraform_state.locked_by_user.to_global_id.to_s) + + expect(version['id']).to eq(latest_version.to_global_id.to_s) + expect(version['createdAt']).to eq(latest_version.created_at.iso8601) + expect(version['updatedAt']).to eq(latest_version.updated_at.iso8601) + expect(version.dig('createdByUser', 'id')).to eq(latest_version.created_by_user.to_global_id.to_s) + expect(version.dig('job', 'name')).to eq(latest_version.build.name) + end + + it 'returns count of terraform states' do + count = data.dig('count') + expect(count).to be(project.terraform_states.size) + end + + context 'unauthorized users' do + let(:current_user) { nil } + + it { expect(data).to be_nil } + end +end diff --git a/spec/requests/api/graphql/read_only_spec.rb b/spec/requests/api/graphql/read_only_spec.rb index ce8a3f6ef5c..d2a45603886 100644 --- a/spec/requests/api/graphql/read_only_spec.rb +++ b/spec/requests/api/graphql/read_only_spec.rb @@ -3,55 +3,11 @@ require 'spec_helper' RSpec.describe 'Requests on a read-only node' do - include GraphqlHelpers - - before do - allow(Gitlab::Database).to receive(:read_only?) { true } - end - - context 'mutations' do - let(:current_user) { note.author } - let!(:note) { create(:note) } - - let(:mutation) do - variables = { - id: GitlabSchema.id_from_object(note).to_s - } - - graphql_mutation(:destroy_note, variables) - end - - def mutation_response - graphql_mutation_response(:destroy_note) - end - - it 'disallows the query' do - post_graphql_mutation(mutation, current_user: current_user) - - expect(json_response['errors'].first['message']).to eq(Mutations::BaseMutation::ERROR_MESSAGE) - end - - it 'does not destroy the Note' do - expect do - post_graphql_mutation(mutation, current_user: current_user) - end.not_to change { Note.count } - end - end - - context 'read-only queries' do - let(:current_user) { create(:user) } - let(:project) { create(:project, :repository) } - + context 'when db is read-only' do before do - project.add_developer(current_user) + allow(Gitlab::Database).to receive(:read_only?) { true } end - it 'allows the query' do - query = graphql_query_for('project', 'fullPath' => project.full_path) - - post_graphql(query, current_user: current_user) - - expect(graphql_data['project']).not_to be_nil - end + it_behaves_like 'graphql on a read-only GitLab instance' end end diff --git a/spec/requests/api/graphql/terraform/state/delete_spec.rb b/spec/requests/api/graphql/terraform/state/delete_spec.rb new file mode 100644 index 00000000000..35927d03b49 --- /dev/null +++ b/spec/requests/api/graphql/terraform/state/delete_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'delete a terraform state' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user, maintainer_projects: [project]) } + + let(:state) { create(:terraform_state, project: project) } + let(:mutation) { graphql_mutation(:terraform_state_delete, id: state.to_global_id.to_s) } + + before do + post_graphql_mutation(mutation, current_user: user) + end + + include_examples 'a working graphql query' + + it 'deletes the state' do + expect { state.reload }.to raise_error(ActiveRecord::RecordNotFound) + end +end diff --git a/spec/requests/api/graphql/terraform/state/lock_spec.rb b/spec/requests/api/graphql/terraform/state/lock_spec.rb new file mode 100644 index 00000000000..e4d3b6336ab --- /dev/null +++ b/spec/requests/api/graphql/terraform/state/lock_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'lock a terraform state' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user, maintainer_projects: [project]) } + + let(:state) { create(:terraform_state, project: project) } + let(:mutation) { graphql_mutation(:terraform_state_lock, id: state.to_global_id.to_s) } + + before do + expect(state).not_to be_locked + post_graphql_mutation(mutation, current_user: user) + end + + include_examples 'a working graphql query' + + it 'locks the state' do + expect(state.reload).to be_locked + expect(state.locked_by_user).to eq(user) + end +end diff --git a/spec/requests/api/graphql/terraform/state/unlock_spec.rb b/spec/requests/api/graphql/terraform/state/unlock_spec.rb new file mode 100644 index 00000000000..e90730f2d8f --- /dev/null +++ b/spec/requests/api/graphql/terraform/state/unlock_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'unlock a terraform state' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user, maintainer_projects: [project]) } + + let(:state) { create(:terraform_state, :locked, project: project) } + let(:mutation) { graphql_mutation(:terraform_state_unlock, id: state.to_global_id.to_s) } + + before do + expect(state).to be_locked + post_graphql_mutation(mutation, current_user: user) + end + + include_examples 'a working graphql query' + + it 'unlocks the state' do + expect(state.reload).not_to be_locked + end +end diff --git a/spec/requests/api/graphql/user/group_member_query_spec.rb b/spec/requests/api/graphql/user/group_member_query_spec.rb index 3a16d962214..e47cef8cc37 100644 --- a/spec/requests/api/graphql/user/group_member_query_spec.rb +++ b/spec/requests/api/graphql/user/group_member_query_spec.rb @@ -19,6 +19,7 @@ RSpec.describe 'GroupMember' do } HEREDOC end + let_it_be(:query) do graphql_query_for('user', { id: member.user.to_global_id.to_s }, query_graphql_field("groupMemberships", {}, fields)) end diff --git a/spec/requests/api/graphql/user/project_member_query_spec.rb b/spec/requests/api/graphql/user/project_member_query_spec.rb index 0790e148caf..01827e94d5d 100644 --- a/spec/requests/api/graphql/user/project_member_query_spec.rb +++ b/spec/requests/api/graphql/user/project_member_query_spec.rb @@ -19,6 +19,7 @@ RSpec.describe 'ProjectMember' do } HEREDOC end + let_it_be(:query) do graphql_query_for('user', { id: member.user.to_global_id.to_s }, query_graphql_field("projectMemberships", {}, fields)) end diff --git a/spec/requests/api/graphql/user_query_spec.rb b/spec/requests/api/graphql/user_query_spec.rb index 79debd0b7ef..738e120549e 100644 --- a/spec/requests/api/graphql/user_query_spec.rb +++ b/spec/requests/api/graphql/user_query_spec.rb @@ -32,22 +32,27 @@ RSpec.describe 'getting user information' do create(:merge_request, :unique_branches, :unique_author, source_project: project_a, assignees: [user]) end + let_it_be(:assigned_mr_b) do create(:merge_request, :unique_branches, :unique_author, source_project: project_b, assignees: [user]) end + let_it_be(:assigned_mr_c) do create(:merge_request, :unique_branches, :unique_author, source_project: project_b, assignees: [user]) end + let_it_be(:authored_mr) do create(:merge_request, :unique_branches, source_project: project_a, author: user) end + let_it_be(:authored_mr_b) do create(:merge_request, :unique_branches, source_project: project_b, author: user) end + let_it_be(:authored_mr_c) do create(:merge_request, :unique_branches, source_project: project_b, author: user) @@ -59,6 +64,7 @@ RSpec.describe 'getting user information' do let(:user_params) { { username: user.username } } before do + create(:user_status, user: user) post_graphql(query, current_user: current_user) end @@ -76,9 +82,15 @@ RSpec.describe 'getting user information' do 'username' => presenter.username, 'webUrl' => presenter.web_url, 'avatarUrl' => presenter.avatar_url, - 'status' => presenter.status, 'email' => presenter.email )) + + expect(graphql_data['user']['status']).to match( + a_hash_including( + 'emoji' => presenter.status.emoji, + 'message' => presenter.status.message, + 'availability' => presenter.status.availability.upcase + )) end describe 'assignedMergeRequests' do diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb index 94a66f54e4d..5dc8edb87e9 100644 --- a/spec/requests/api/graphql_spec.rb +++ b/spec/requests/api/graphql_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe 'GraphQL' do include GraphqlHelpers - let(:query) { graphql_query_for('echo', 'text' => 'Hello world' ) } + let(:query) { graphql_query_for('echo', text: 'Hello world' ) } context 'logging' do shared_examples 'logging a graphql query' do diff --git a/spec/requests/api/group_labels_spec.rb b/spec/requests/api/group_labels_spec.rb index f965a845bbe..72621e2ce5e 100644 --- a/spec/requests/api/group_labels_spec.rb +++ b/spec/requests/api/group_labels_spec.rb @@ -7,60 +7,97 @@ RSpec.describe API::GroupLabels do let(:group) { create(:group) } let(:subgroup) { create(:group, parent: group) } let!(:group_member) { create(:group_member, group: group, user: user) } - let!(:group_label1) { create(:group_label, title: 'feature', group: group) } + let!(:group_label1) { create(:group_label, title: 'feature-label', group: group) } let!(:group_label2) { create(:group_label, title: 'bug', group: group) } - let!(:subgroup_label) { create(:group_label, title: 'support', group: subgroup) } + let!(:subgroup_label) { create(:group_label, title: 'support-label', group: subgroup) } describe 'GET :id/labels' do - it 'returns all available labels for the group' do - get api("/groups/#{group.id}/labels", user) + context 'get current group labels' do + let(:request) { get api("/groups/#{group.id}/labels", user) } + let(:expected_labels) { [group_label1.name, group_label2.name] } - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response).to all(match_schema('public_api/v4/labels/label')) - expect(json_response.size).to eq(2) - expect(json_response.map {|r| r['name'] }).to contain_exactly('feature', 'bug') - end - - context 'when the with_counts parameter is set' do - it 'includes counts in the response' do - get api("/groups/#{group.id}/labels", user), params: { with_counts: true } - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response).to all(match_schema('public_api/v4/labels/label_with_counts')) - expect(json_response.size).to eq(2) - expect(json_response.map { |r| r['open_issues_count'] }).to contain_exactly(0, 0) + it_behaves_like 'fetches labels' + + context 'when search param is provided' do + let(:request) { get api("/groups/#{group.id}/labels?search=lab", user) } + let(:expected_labels) { [group_label1.name] } + + it_behaves_like 'fetches labels' end - end - end - describe 'GET :subgroup_id/labels' do - context 'when the include_ancestor_groups parameter is not set' do - it 'returns all available labels for the group and ancestor groups' do - get api("/groups/#{subgroup.id}/labels", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response).to all(match_schema('public_api/v4/labels/label')) - expect(json_response.size).to eq(3) - expect(json_response.map {|r| r['name'] }).to contain_exactly('feature', 'bug', 'support') + context 'when the with_counts parameter is set' do + it 'includes counts in the response' do + get api("/groups/#{group.id}/labels", user), params: { with_counts: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response).to all(match_schema('public_api/v4/labels/label_with_counts')) + expect(json_response.size).to eq(2) + expect(json_response.map { |r| r['open_issues_count'] }).to contain_exactly(0, 0) + end + end + + context 'when include_descendant_groups param is provided' do + let!(:project) { create(:project, group: group) } + let!(:project_label1) { create(:label, title: 'project-label1', project: project, priority: 3) } + let!(:project_label2) { create(:label, title: 'project-bug', project: project) } + + let(:request) { get api("/groups/#{group.id}/labels", user), params: { include_descendant_groups: true } } + let(:expected_labels) { [group_label1.name, group_label2.name, subgroup_label.name] } + + it_behaves_like 'fetches labels' + + context 'when search param is provided' do + let(:request) { get api("/groups/#{group.id}/labels", user), params: { search: 'lab', include_descendant_groups: true } } + let(:expected_labels) { [group_label1.name, subgroup_label.name] } + + it_behaves_like 'fetches labels' + end + + context 'when only_group_labels param is false' do + let(:request) { get api("/groups/#{group.id}/labels", user), params: { include_descendant_groups: true, only_group_labels: false } } + let(:expected_labels) { [group_label1.name, group_label2.name, subgroup_label.name, project_label1.name, project_label2.name] } + + it_behaves_like 'fetches labels' + + context 'when search param is provided' do + let(:request) { get api("/groups/#{group.id}/labels", user), params: { search: 'lab', include_descendant_groups: true, only_group_labels: false } } + let(:expected_labels) { [group_label1.name, subgroup_label.name, project_label1.name] } + + it_behaves_like 'fetches labels' + end + end end end - context 'when the include_ancestor_groups parameter is set to false' do - it 'returns all available labels for the group but not for ancestor groups' do - get api("/groups/#{subgroup.id}/labels", user), params: { include_ancestor_groups: false } + describe 'with subgroup labels' do + context 'when the include_ancestor_groups parameter is not set' do + let(:request) { get api("/groups/#{subgroup.id}/labels", user) } + let(:expected_labels) { [group_label1.name, group_label2.name, subgroup_label.name] } + + it_behaves_like 'fetches labels' + + context 'when search param is provided' do + let(:request) { get api("/groups/#{subgroup.id}/labels?search=lab", user) } + let(:expected_labels) { [group_label1.name, subgroup_label.name] } + + it_behaves_like 'fetches labels' + end + end + + context 'when the include_ancestor_groups parameter is set to false' do + let(:request) { get api("/groups/#{subgroup.id}/labels", user), params: { include_ancestor_groups: false } } + let(:expected_labels) { [subgroup_label.name] } + + it_behaves_like 'fetches labels' + + context 'when search param is provided' do + let(:request) { get api("/groups/#{subgroup.id}/labels?search=lab", user), params: { include_ancestor_groups: false } } + let(:expected_labels) { [subgroup_label.name] } - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response).to all(match_schema('public_api/v4/labels/label')) - expect(json_response.size).to eq(1) - expect(json_response.map {|r| r['name'] }).to contain_exactly('support') + it_behaves_like 'fetches labels' + end end end end @@ -223,7 +260,7 @@ RSpec.describe API::GroupLabels do expect(response).to have_gitlab_http_status(:ok) expect(subgroup.labels[0].name).to eq('New Label') - expect(group_label1.name).to eq('feature') + expect(group_label1.name).to eq(group_label1.title) end it 'returns 404 if label does not exist' do @@ -278,7 +315,7 @@ RSpec.describe API::GroupLabels do expect(response).to have_gitlab_http_status(:ok) expect(subgroup.labels[0].name).to eq('New Label') - expect(group_label1.name).to eq('feature') + expect(group_label1.name).to eq(group_label1.title) end it 'returns 404 if label does not exist' do diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb index bbfb17fe753..5bbcb0c1950 100644 --- a/spec/requests/api/import_github_spec.rb +++ b/spec/requests/api/import_github_spec.rb @@ -57,6 +57,22 @@ RSpec.describe API::ImportGithub do expect(json_response['name']).to eq(project.name) end + it 'returns 201 response when the project is imported successfully from GHE' do + allow(Gitlab::LegacyGithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: project)) + + post api("/import/github", user), params: { + target_namespace: user.namespace_path, + personal_access_token: token, + repo_id: non_existing_record_id, + github_hostname: "https://github.somecompany.com/" + } + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to be_a Hash + expect(json_response['name']).to eq(project.name) + end + it 'returns 422 response when user can not create projects in the chosen namespace' do other_namespace = create(:group, name: 'other_namespace') diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index ab5f09305ce..6fe77727702 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -253,6 +253,7 @@ RSpec.describe API::Internal::Base do describe "POST /internal/lfs_authenticate" do before do + stub_lfs_setting(enabled: true) project.add_developer(user) end @@ -293,6 +294,33 @@ RSpec.describe API::Internal::Base do expect(response).to have_gitlab_http_status(:not_found) end + + it 'returns a 404 when LFS is disabled on the project' do + project.update!(lfs_enabled: false) + lfs_auth_user(user.id, project) + + expect(response).to have_gitlab_http_status(:not_found) + end + + context 'other repository types' do + it 'returns the correct information for a project wiki' do + wiki = create(:project_wiki, project: project) + lfs_auth_user(user.id, wiki) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['username']).to eq(user.username) + expect(json_response['repository_http_path']).to eq(wiki.http_url_to_repo) + expect(json_response['expires_in']).to eq(Gitlab::LfsToken::DEFAULT_EXPIRE_TIME) + expect(Gitlab::LfsToken.new(user).token_valid?(json_response['lfs_token'])).to be_truthy + end + + it 'returns a 404 when the container does not support LFS' do + snippet = create(:project_snippet) + lfs_auth_user(user.id, snippet) + + expect(response).to have_gitlab_http_status(:not_found) + end + end end context 'deploy key' do diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb index e58eba02132..9a63e2a8ed5 100644 --- a/spec/requests/api/internal/pages_spec.rb +++ b/spec/requests/api/internal/pages_spec.rb @@ -12,6 +12,7 @@ RSpec.describe API::Internal::Pages do before do allow(Gitlab::Pages).to receive(:secret).and_return(pages_secret) + stub_pages_object_storage(::Pages::DeploymentUploader) end describe "GET /internal/pages/status" do @@ -38,6 +39,12 @@ RSpec.describe API::Internal::Pages do get api("/internal/pages"), headers: headers, params: { host: host } end + around do |example| + freeze_time do + example.run + end + end + context 'not authenticated' do it 'responds with 401 Unauthorized' do query_host('pages.gitlab.io') @@ -55,7 +62,9 @@ RSpec.describe API::Internal::Pages do end def deploy_pages(project) + deployment = create(:pages_deployment, project: project) project.mark_pages_as_deployed + project.update_pages_deployment!(deployment) end context 'domain does not exist' do @@ -182,6 +191,7 @@ RSpec.describe API::Internal::Pages do expect(json_response['certificate']).to eq(pages_domain.certificate) expect(json_response['key']).to eq(pages_domain.key) + deployment = project.pages_metadatum.pages_deployment expect(json_response['lookup_paths']).to eq( [ { @@ -190,8 +200,12 @@ RSpec.describe API::Internal::Pages do 'https_only' => false, 'prefix' => '/', 'source' => { - 'type' => 'file', - 'path' => 'gitlab-org/gitlab-ce/public/' + 'type' => 'zip', + 'path' => deployment.file.url(expire_at: 1.day.from_now), + 'global_id' => "gid://gitlab/PagesDeployment/#{deployment.id}", + 'sha256' => deployment.file_sha256, + 'file_size' => deployment.size, + 'file_count' => deployment.file_count } } ] @@ -218,6 +232,7 @@ RSpec.describe API::Internal::Pages do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('internal/pages/virtual_domain') + deployment = project.pages_metadatum.pages_deployment expect(json_response['lookup_paths']).to eq( [ { @@ -226,8 +241,12 @@ RSpec.describe API::Internal::Pages do 'https_only' => false, 'prefix' => '/myproject/', 'source' => { - 'type' => 'file', - 'path' => 'mygroup/myproject/public/' + 'type' => 'zip', + 'path' => deployment.file.url(expire_at: 1.day.from_now), + 'global_id' => "gid://gitlab/PagesDeployment/#{deployment.id}", + 'sha256' => deployment.file_sha256, + 'file_size' => deployment.size, + 'file_count' => deployment.file_count } } ] @@ -235,6 +254,20 @@ RSpec.describe API::Internal::Pages do end end + it 'avoids N+1 queries' do + project = create(:project, group: group) + deploy_pages(project) + + control = ActiveRecord::QueryRecorder.new { query_host('mygroup.gitlab-pages.io') } + + 3.times do + project = create(:project, group: group) + deploy_pages(project) + end + + expect { query_host('mygroup.gitlab-pages.io') }.not_to exceed_query_limit(control) + end + context 'group root project' do it 'responds with the correct domain configuration' do project = create(:project, group: group, name: 'mygroup.gitlab-pages.io') @@ -245,6 +278,7 @@ RSpec.describe API::Internal::Pages do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('internal/pages/virtual_domain') + deployment = project.pages_metadatum.pages_deployment expect(json_response['lookup_paths']).to eq( [ { @@ -253,8 +287,12 @@ RSpec.describe API::Internal::Pages do 'https_only' => false, 'prefix' => '/', 'source' => { - 'type' => 'file', - 'path' => 'mygroup/mygroup.gitlab-pages.io/public/' + 'type' => 'zip', + 'path' => deployment.file.url(expire_at: 1.day.from_now), + 'global_id' => "gid://gitlab/PagesDeployment/#{deployment.id}", + 'sha256' => deployment.file_sha256, + 'file_size' => deployment.size, + 'file_count' => deployment.file_count } } ] diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb new file mode 100644 index 00000000000..75586970abb --- /dev/null +++ b/spec/requests/api/invitations_spec.rb @@ -0,0 +1,301 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Invitations do + let(:maintainer) { create(:user, username: 'maintainer_user') } + let(:developer) { create(:user) } + let(:access_requester) { create(:user) } + let(:stranger) { create(:user) } + let(:email) { 'email1@example.com' } + let(:email2) { 'email2@example.com' } + + let(:project) do + create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project| + project.add_developer(developer) + project.add_maintainer(maintainer) + project.request_access(access_requester) + end + end + + let!(:group) do + create(:group, :public) do |group| + group.add_developer(developer) + group.add_owner(maintainer) + group.request_access(access_requester) + end + end + + def invitations_url(source, user) + api("/#{source.model_name.plural}/#{source.id}/invitations", user) + end + + shared_examples 'POST /:source_type/:id/invitations' do |source_type| + context "with :source_type == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) do + post invitations_url(source, stranger), + params: { email: email, access_level: Member::MAINTAINER } + end + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger developer].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + + post invitations_url(source, user), params: { email: email, access_level: Member::MAINTAINER } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + end + + context 'when authenticated as a maintainer/owner' do + context 'and new member is already a requester' do + it 'does not transform the requester into a proper member' do + expect do + post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer), + params: { email: email, access_level: Member::MAINTAINER } + + expect(response).to have_gitlab_http_status(:created) + end.not_to change { source.members.count } + end + end + + it 'invites a new member' do + expect do + post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer), + params: { email: email, access_level: Member::DEVELOPER } + + expect(response).to have_gitlab_http_status(:created) + end.to change { source.requesters.count }.by(1) + end + + it 'invites a list of new email addresses' do + expect do + email_list = [email, email2].join(',') + + post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer), + params: { email: email_list, access_level: Member::DEVELOPER } + + expect(response).to have_gitlab_http_status(:created) + end.to change { source.requesters.count }.by(2) + end + end + + context 'access levels' do + it 'does not create the member if group level is higher' do + parent = create(:group) + + group.update!(parent: parent) + project.update!(group: group) + parent.add_developer(stranger) + + post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer), + params: { email: stranger.email, access_level: Member::REPORTER } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['message'][stranger.email]).to eq("Access level should be greater than or equal to Developer inherited membership from group #{parent.name}") + end + + it 'creates the member if group level is lower' do + parent = create(:group) + + group.update!(parent: parent) + project.update!(group: group) + parent.add_developer(stranger) + + post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer), + params: { email: stranger.email, access_level: Member::MAINTAINER } + + expect(response).to have_gitlab_http_status(:created) + end + end + + context 'access expiry date' do + subject do + post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer), + params: { email: email, access_level: Member::DEVELOPER, expires_at: expires_at } + end + + context 'when set to a date in the past' do + let(:expires_at) { 2.days.ago.to_date } + + it 'does not create a member' do + expect do + subject + end.not_to change { source.members.count } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['message'][email]).to eq('Expires at cannot be a date in the past') + end + end + + context 'when set to a date in the future' do + let(:expires_at) { 2.days.from_now.to_date } + + it 'invites a member' do + expect do + subject + end.to change { source.requesters.count }.by(1) + + expect(response).to have_gitlab_http_status(:created) + end + end + end + + it "returns a message if member already exists" do + post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer), + params: { email: maintainer.email, access_level: Member::MAINTAINER } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['message'][maintainer.email]).to eq("Already a member of #{source.name}") + end + + it 'returns 404 when the email is not valid' do + post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer), + params: { email: '', access_level: Member::MAINTAINER } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['message']).to eq('Email cannot be blank') + end + + it 'returns 404 when the email list is not a valid format' do + post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer), + params: { email: 'email1@example.com,not-an-email', access_level: Member::MAINTAINER } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('email contains an invalid email address') + end + + it 'returns 400 when email is not given' do + post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer), + params: { access_level: Member::MAINTAINER } + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns 400 when access_level is not given' do + post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer), + params: { email: email } + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'returns 400 when access_level is not valid' do + post invitations_url(source, maintainer), + params: { email: email, access_level: non_existing_record_access_level } + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end + + describe 'POST /projects/:id/invitations' do + it_behaves_like 'POST /:source_type/:id/invitations', 'project' do + let(:source) { project } + end + end + + describe 'POST /groups/:id/invitations' do + it_behaves_like 'POST /:source_type/:id/invitations', 'group' do + let(:source) { group } + end + end + + shared_examples 'GET /:source_type/:id/invitations' do |source_type| + context "with :source_type == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { get invitations_url(source, stranger) } + end + + %i[maintainer developer access_requester stranger].each do |type| + context "when authenticated as a #{type}" do + it 'returns 200' do + user = public_send(type) + + get invitations_url(source, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(0) + end + end + end + + it 'avoids N+1 queries' do + # Establish baseline + get invitations_url(source, maintainer) + + control = ActiveRecord::QueryRecorder.new do + get invitations_url(source, maintainer) + end + + invite_member_by_email(source, source_type, email, maintainer) + + expect do + get invitations_url(source, maintainer) + end.not_to exceed_query_limit(control) + end + + it 'does not find confirmed members' do + get invitations_url(source, developer) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(0) + expect(json_response.map { |u| u['id'] }).not_to match_array [maintainer.id, developer.id] + end + + it 'finds all members with no query string specified' do + invite_member_by_email(source, source_type, email, developer) + invite_member_by_email(source, source_type, email2, developer) + + get invitations_url(source, developer), params: { query: '' } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + + expect(json_response).to be_an Array + expect(json_response.count).to eq(2) + expect(json_response.map { |u| u['invite_email'] }).to match_array [email, email2] + end + + it 'finds the invitation by invite_email with query string' do + invite_member_by_email(source, source_type, email, developer) + invite_member_by_email(source, source_type, email2, developer) + + get invitations_url(source, developer), params: { query: email } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.count).to eq(1) + expect(json_response.first['invite_email']).to eq(email) + expect(json_response.first['created_by_name']).to eq(developer.name) + expect(json_response.first['user_name']).to eq(nil) + end + + def invite_member_by_email(source, source_type, email, created_by) + create(:"#{source_type}_member", invite_token: '123', invite_email: email, source: source, user: nil, created_by: created_by) + end + end + end + + describe 'GET /projects/:id/invitations' do + it_behaves_like 'GET /:source_type/:id/invitations', 'project' do + let(:source) { project } + end + end + + describe 'GET /groups/:id/invitations' do + it_behaves_like 'GET /:source_type/:id/invitations', 'group' do + let(:source) { group } + end + end +end diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb index 4228ca2d5fd..da0bae8d5e7 100644 --- a/spec/requests/api/issues/get_project_issues_spec.rb +++ b/spec/requests/api/issues/get_project_issues_spec.rb @@ -54,11 +54,13 @@ RSpec.describe API::Issues do let_it_be(:label) do create(:label, title: 'label', color: '#FFAABB', project: project) end + let!(:label_link) { create(:label_link, label: label, target: issue) } let(:milestone) { create(:milestone, title: '1.0.0', project: project) } let_it_be(:empty_milestone) do create(:milestone, title: '2.0.0', project: project) end + let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } let(:no_milestone_title) { 'None' } diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb index b8cbddd9ed4..0fe68be027c 100644 --- a/spec/requests/api/issues/issues_spec.rb +++ b/spec/requests/api/issues/issues_spec.rb @@ -54,11 +54,13 @@ RSpec.describe API::Issues do let_it_be(:label) do create(:label, title: 'label', color: '#FFAABB', project: project) end + let!(:label_link) { create(:label_link, label: label, target: issue) } let(:milestone) { create(:milestone, title: '1.0.0', project: project) } let_it_be(:empty_milestone) do create(:milestone, title: '2.0.0', project: project) end + let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } let(:no_milestone_title) { 'None' } diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb index a7fe4d4509a..5b3e2363669 100644 --- a/spec/requests/api/issues/post_projects_issues_spec.rb +++ b/spec/requests/api/issues/post_projects_issues_spec.rb @@ -53,11 +53,13 @@ RSpec.describe API::Issues do let_it_be(:label) do create(:label, title: 'label', color: '#FFAABB', project: project) end + let!(:label_link) { create(:label_link, label: label, target: issue) } let(:milestone) { create(:milestone, title: '1.0.0', project: project) } let_it_be(:empty_milestone) do create(:milestone, title: '2.0.0', project: project) end + let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } let(:no_milestone_title) { 'None' } diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index fc674fca9b2..b368f6e329c 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -178,8 +178,8 @@ RSpec.describe API::Labels do end describe 'GET /projects/:id/labels' do - let(:group) { create(:group) } - let!(:group_label) { create(:group_label, title: 'feature', group: group) } + let_it_be(:group) { create(:group) } + let_it_be(:group_label) { create(:group_label, title: 'feature label', group: group) } before do project.update!(group: group) @@ -250,49 +250,41 @@ RSpec.describe API::Labels do end end - context 'when the include_ancestor_groups parameter is not set' do - let(:group) { create(:group) } - let!(:group_label) { create(:group_label, title: 'feature', group: group) } - let(:subgroup) { create(:group, parent: group) } - let!(:subgroup_label) { create(:group_label, title: 'support', group: subgroup) } + context 'with subgroups' do + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:subgroup_label) { create(:group_label, title: 'support label', group: subgroup) } before do subgroup.add_owner(user) project.update!(group: subgroup) end - it 'returns all available labels for the project, parent group and ancestor groups' do - get api("/projects/#{project.id}/labels", user) + context 'when the include_ancestor_groups parameter is not set' do + let(:request) { get api("/projects/#{project.id}/labels", user) } + let(:expected_labels) { [priority_label.name, group_label.name, subgroup_label.name, label1.name] } - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response).to all(match_schema('public_api/v4/labels/label')) - expect(json_response.size).to eq(4) - expect(json_response.map {|r| r['name'] }).to contain_exactly(group_label.name, subgroup_label.name, priority_label.name, label1.name) - end - end + it_behaves_like 'fetches labels' - context 'when the include_ancestor_groups parameter is set to false' do - let(:group) { create(:group) } - let!(:group_label) { create(:group_label, title: 'feature', group: group) } - let(:subgroup) { create(:group, parent: group) } - let!(:subgroup_label) { create(:group_label, title: 'support', group: subgroup) } + context 'when search param is provided' do + let(:request) { get api("/projects/#{project.id}/labels?search=lab", user) } + let(:expected_labels) { [group_label.name, subgroup_label.name, label1.name] } - before do - subgroup.add_owner(user) - project.update!(group: subgroup) + it_behaves_like 'fetches labels' + end end - it 'returns all available labels for the project and the parent group only' do - get api("/projects/#{project.id}/labels", user), params: { include_ancestor_groups: false } + context 'when the include_ancestor_groups parameter is set to false' do + let(:request) { get api("/projects/#{project.id}/labels", user), params: { include_ancestor_groups: false } } + let(:expected_labels) { [subgroup_label.name, priority_label.name, label1.name] } - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response).to all(match_schema('public_api/v4/labels/label')) - expect(json_response.size).to eq(3) - expect(json_response.map {|r| r['name'] }).to contain_exactly(subgroup_label.name, priority_label.name, label1.name) + it_behaves_like 'fetches labels' + + context 'when search param is provided' do + let(:request) { get api("/projects/#{project.id}/labels?search=lab", user), params: { include_ancestor_groups: false } } + let(:expected_labels) { [subgroup_label.name, label1.name] } + + it_behaves_like 'fetches labels' + end end end end @@ -513,7 +505,7 @@ RSpec.describe API::Labels do end describe 'PUT /projects/:id/labels/promote' do - let(:group) { create(:group) } + let_it_be(:group) { create(:group) } before do group.add_owner(user) diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb index 9890cdc20c0..aecbcfb5b5a 100644 --- a/spec/requests/api/lint_spec.rb +++ b/spec/requests/api/lint_spec.rb @@ -9,12 +9,13 @@ RSpec.describe API::Lint do File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) end - it 'passes validation' do + it 'passes validation without warnings or errors' do post api('/ci/lint'), params: { content: yaml_content } expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Hash expect(json_response['status']).to eq('valid') + expect(json_response['warnings']).to eq([]) expect(json_response['errors']).to eq([]) end @@ -26,6 +27,20 @@ RSpec.describe API::Lint do end end + context 'with valid .gitlab-ci.yaml with warnings' do + let(:yaml_content) { { job: { script: 'ls', rules: [{ when: 'always' }] } }.to_yaml } + + it 'passes validation but returns warnings' do + post api('/ci/lint'), params: { content: yaml_content } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['status']).to eq('valid') + expect(json_response['warnings']).not_to be_empty + expect(json_response['status']).to eq('valid') + expect(json_response['errors']).to eq([]) + end + end + context 'with an invalid .gitlab_ci.yml' do context 'with invalid syntax' do let(:yaml_content) { 'invalid content' } @@ -35,6 +50,7 @@ RSpec.describe API::Lint do expect(response).to have_gitlab_http_status(:ok) expect(json_response['status']).to eq('invalid') + expect(json_response['warnings']).to eq([]) expect(json_response['errors']).to eq(['Invalid configuration format']) end @@ -54,6 +70,7 @@ RSpec.describe API::Lint do expect(response).to have_gitlab_http_status(:ok) expect(json_response['status']).to eq('invalid') + expect(json_response['warnings']).to eq([]) expect(json_response['errors']).to eq(['jobs config should contain at least one visible job']) end @@ -82,7 +99,18 @@ RSpec.describe API::Lint do let(:project) { create(:project, :repository) } let(:dry_run) { nil } - RSpec.shared_examples 'valid config' do + RSpec.shared_examples 'valid config with warnings' do + it 'passes validation with warnings' do + ci_lint + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['valid']).to eq(true) + expect(json_response['errors']).to eq([]) + expect(json_response['warnings']).not_to be_empty + end + end + + RSpec.shared_examples 'valid config without warnings' do it 'passes validation' do ci_lint @@ -94,6 +122,7 @@ RSpec.describe API::Lint do expect(json_response).to be_an Hash expect(json_response['merged_yaml']).to eq(expected_yaml) expect(json_response['valid']).to eq(true) + expect(json_response['warnings']).to eq([]) expect(json_response['errors']).to eq([]) end end @@ -105,6 +134,7 @@ RSpec.describe API::Lint do expect(response).to have_gitlab_http_status(:ok) expect(json_response['merged_yaml']).to eq(yaml_content) expect(json_response['valid']).to eq(false) + expect(json_response['warnings']).to eq([]) expect(json_response['errors']).to eq(['jobs config should contain at least one visible job']) end end @@ -157,6 +187,7 @@ RSpec.describe API::Lint do expect(response).to have_gitlab_http_status(:ok) expect(json_response['merged_yaml']).to eq(nil) expect(json_response['valid']).to eq(false) + expect(json_response['warnings']).to eq([]) expect(json_response['errors']).to eq(['Insufficient permissions to create a new pipeline']) end end @@ -186,7 +217,7 @@ RSpec.describe API::Lint do ) end - it_behaves_like 'valid config' + it_behaves_like 'valid config without warnings' end end end @@ -242,13 +273,19 @@ RSpec.describe API::Lint do context 'when running as dry run' do let(:dry_run) { true } - it_behaves_like 'valid config' + it_behaves_like 'valid config without warnings' end context 'when running static validation' do let(:dry_run) { false } - it_behaves_like 'valid config' + it_behaves_like 'valid config without warnings' + end + + context 'With warnings' do + let(:yaml_content) { { job: { script: 'ls', rules: [{ when: 'always' }] } }.to_yaml } + + it_behaves_like 'valid config with warnings' end end @@ -275,4 +312,167 @@ RSpec.describe API::Lint do end end end + + describe 'POST /projects/:id/ci/lint' do + subject(:ci_lint) { post api("/projects/#{project.id}/ci/lint", api_user), params: { dry_run: dry_run, content: yaml_content } } + + let(:project) { create(:project, :repository) } + let(:dry_run) { nil } + + let_it_be(:api_user) { create(:user) } + + let_it_be(:yaml_content) do + { include: { local: 'another-gitlab-ci.yml' }, test: { stage: 'test', script: 'echo 1' } }.to_yaml + end + + let_it_be(:included_content) do + { another_test: { stage: 'test', script: 'echo 1' } }.to_yaml + end + + RSpec.shared_examples 'valid project config' do + it 'passes validation' do + ci_lint + + included_config = YAML.safe_load(included_content, [Symbol]) + root_config = YAML.safe_load(yaml_content, [Symbol]) + expected_yaml = included_config.merge(root_config).except(:include).to_yaml + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Hash + expect(json_response['merged_yaml']).to eq(expected_yaml) + expect(json_response['valid']).to eq(true) + expect(json_response['errors']).to eq([]) + end + end + + RSpec.shared_examples 'invalid project config' do + it 'responds with errors about invalid configuration' do + ci_lint + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['merged_yaml']).to eq(yaml_content) + expect(json_response['valid']).to eq(false) + expect(json_response['errors']).to eq(['jobs config should contain at least one visible job']) + end + end + + context 'when unauthenticated' do + let_it_be(:api_user) { nil } + + it 'returns authentication error' do + ci_lint + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when authenticated as non-member' do + context 'when project is private' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + it 'returns authentication error' do + ci_lint + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when project is public' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end + + context 'when running as dry run' do + let(:dry_run) { true } + + it 'returns pipeline creation error' do + ci_lint + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['merged_yaml']).to eq(nil) + expect(json_response['valid']).to eq(false) + expect(json_response['errors']).to eq(['Insufficient permissions to create a new pipeline']) + end + end + + context 'when running static validation' do + let(:dry_run) { false } + + before do + project.repository.create_file( + project.creator, + 'another-gitlab-ci.yml', + included_content, + message: 'Automatically created another-gitlab-ci.yml', + branch_name: 'master' + ) + end + + it_behaves_like 'valid project config' + end + end + end + + context 'when authenticated as project guest' do + before do + project.add_guest(api_user) + end + + it 'returns authentication error' do + ci_lint + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when authenticated as project developer' do + before do + project.add_developer(api_user) + end + + context 'with valid .gitlab-ci.yml content' do + before do + project.repository.create_file( + project.creator, + 'another-gitlab-ci.yml', + included_content, + message: 'Automatically created another-gitlab-ci.yml', + branch_name: 'master' + ) + end + + context 'when running as dry run' do + let(:dry_run) { true } + + it_behaves_like 'valid project config' + end + + context 'when running static validation' do + let(:dry_run) { false } + + it_behaves_like 'valid project config' + end + end + + context 'with invalid .gitlab-ci.yml content' do + let(:yaml_content) do + { image: 'ruby:2.7', services: ['postgres'] }.to_yaml + end + + context 'when running as dry run' do + let(:dry_run) { true } + + it_behaves_like 'invalid project config' + end + + context 'when running static validation' do + let(:dry_run) { false } + + it_behaves_like 'invalid project config' + end + end + end + end end diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb index 37748fe5ea7..f9ba819c9aa 100644 --- a/spec/requests/api/maven_packages_spec.rb +++ b/spec/requests/api/maven_packages_spec.rb @@ -92,15 +92,30 @@ RSpec.describe API::MavenPackages do end shared_examples 'downloads with a deploy token' do - it 'allows download with deploy token' do - download_file( - package_file.file_name, - {}, - Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token.token - ) + context 'successful download' do + subject do + download_file( + package_file.file_name, + {}, + Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token.token + ) + end - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq('application/octet-stream') + it 'allows download with deploy token' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + + it 'allows download with deploy token with only write_package_registry scope' do + deploy_token.update!(read_package_registry: false) + + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end end end @@ -355,6 +370,15 @@ RSpec.describe API::MavenPackages do expect(response).to have_gitlab_http_status(:ok) expect(response.media_type).to eq('application/octet-stream') end + + it 'returns the file with only write_package_registry scope' do + deploy_token_for_group.update!(read_package_registry: false) + + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end end end @@ -601,7 +625,7 @@ RSpec.describe API::MavenPackages do upload_file(params: params.merge(job_token: job.token)) expect(response).to have_gitlab_http_status(:ok) - expect(project.reload.packages.last.build_info.pipeline).to eq job.pipeline + expect(project.reload.packages.last.original_build_info.pipeline).to eq job.pipeline end it 'rejects upload without running job token' do diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 047b9423906..919c8d29406 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -7,6 +7,7 @@ RSpec.describe API::Members do let(:developer) { create(:user) } let(:access_requester) { create(:user) } let(:stranger) { create(:user) } + let(:user_with_minimal_access) { create(:user) } let(:project) do create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project| @@ -20,6 +21,7 @@ RSpec.describe API::Members do create(:group, :public) do |group| group.add_developer(developer) group.add_owner(maintainer) + create(:group_member, :minimal_access, source: group, user: user_with_minimal_access) group.request_access(access_requester) end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 506607f4cc2..e7005bd3ec5 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -1312,13 +1312,44 @@ 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) } + let_it_be(:merge_request) do + create( + :merge_request, + :simple, + author: user, + assignees: [user], + source_project: project, + target_project: project, + source_branch: 'markdown', + title: "Test", + created_at: base_time + ) + end - it 'returns the change information of the merge_request' do - get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user) + shared_examples 'find an existing merge request' do + it 'returns the change information of the merge_request' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user) - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['changes'].size).to eq(merge_request.diffs.size) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['changes'].size).to eq(merge_request.diffs.size) + expect(json_response['overflow']).to be_falsy + end + end + + shared_examples 'accesses diffs via raw_diffs' do + let(:params) { {} } + + it 'as expected' do + expect_any_instance_of(MergeRequest) do |merge_request| + expect(merge_request).to receive(:raw_diffs).and_call_original + end + + expect_any_instance_of(MergeRequest) do |merge_request| + expect(merge_request).not_to receive(:diffs) + end + + get(api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user), params: params) + end end it 'returns a 404 when merge_request_iid not found' do @@ -1331,6 +1362,53 @@ RSpec.describe API::MergeRequests do expect(response).to have_gitlab_http_status(:not_found) end + + it_behaves_like 'find an existing merge request' + it_behaves_like 'accesses diffs via raw_diffs' + + it 'returns the overflow status as false' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['overflow']).to be_falsy + end + + context 'when using DB-backed diffs via feature flag' do + before do + stub_feature_flags(mrc_api_use_raw_diffs_from_gitaly: false) + end + + it_behaves_like 'find an existing merge request' + + it 'accesses diffs via DB-backed diffs.diffs' do + expect_any_instance_of(MergeRequest) do |merge_request| + expect(merge_request).to receive(:diffs).and_call_original + end + + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user) + end + + context 'when the diff_collection has overflowed its size limits' do + before do + expect_next_instance_of(Gitlab::Git::DiffCollection) do |diff_collection| + expect(diff_collection).to receive(:overflow?).and_return(true) + end + end + + it 'returns the overflow status as true' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['overflow']).to be_truthy + end + end + + context 'when access_raw_diffs is passed as an option' do + it_behaves_like 'accesses diffs via raw_diffs' do + let(:params) { { access_raw_diffs: true } } + end + end + end end describe 'GET /projects/:id/merge_requests/:merge_request_iid/pipelines' do diff --git a/spec/requests/api/npm_instance_packages_spec.rb b/spec/requests/api/npm_instance_packages_spec.rb new file mode 100644 index 00000000000..8299717b5c7 --- /dev/null +++ b/spec/requests/api/npm_instance_packages_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::NpmInstancePackages do + include_context 'npm api setup' + + describe 'GET /api/v4/packages/npm/*package_name' do + it_behaves_like 'handling get metadata requests' do + let(:url) { api("/packages/npm/#{package_name}") } + end + end + + describe 'GET /api/v4/packages/npm/-/package/*package_name/dist-tags' do + it_behaves_like 'handling get dist tags requests', scope: :instance do + let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags") } + end + end + + describe 'PUT /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do + it_behaves_like 'handling create dist tag requests', scope: :instance do + let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } + end + end + + describe 'DELETE /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do + it_behaves_like 'handling delete dist tag requests', scope: :instance do + let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } + end + end +end diff --git a/spec/requests/api/npm_packages_spec.rb b/spec/requests/api/npm_packages_spec.rb deleted file mode 100644 index 8a3ccd7c6e3..00000000000 --- a/spec/requests/api/npm_packages_spec.rb +++ /dev/null @@ -1,556 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe API::NpmPackages do - include PackagesManagerApiSpecHelpers - include HttpBasicAuthHelpers - - let_it_be(:user) { create(:user) } - let_it_be(:group) { create(:group) } - let_it_be(:project, reload: true) { create(:project, :public, namespace: group) } - let_it_be(:package, reload: true) { create(:npm_package, project: project) } - let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) } - let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } - let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running) } - let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } - let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } - - before do - project.add_developer(user) - end - - shared_examples 'a package that requires auth' do - it 'returns the package info with oauth token' do - get_package_with_token(package) - - expect_a_valid_package_response - end - - it 'returns the package info with running job token' do - get_package_with_job_token(package) - - expect_a_valid_package_response - end - - it 'denies request without running job token' do - job.update!(status: :success) - get_package_with_job_token(package) - - expect(response).to have_gitlab_http_status(:unauthorized) - end - - it 'denies request without oauth token' do - get_package(package) - - expect(response).to have_gitlab_http_status(:forbidden) - end - - it 'returns the package info with deploy token' do - get_package_with_deploy_token(package) - - expect_a_valid_package_response - end - end - - describe 'GET /api/v4/packages/npm/*package_name' do - let_it_be(:package_dependency_link1) { create(:packages_dependency_link, package: package, dependency_type: :dependencies) } - let_it_be(:package_dependency_link2) { create(:packages_dependency_link, package: package, dependency_type: :devDependencies) } - let_it_be(:package_dependency_link3) { create(:packages_dependency_link, package: package, dependency_type: :bundleDependencies) } - let_it_be(:package_dependency_link4) { create(:packages_dependency_link, package: package, dependency_type: :peerDependencies) } - - shared_examples 'returning the npm package info' do - it 'returns the package info' do - get_package(package) - - expect_a_valid_package_response - end - end - - shared_examples 'returning forbidden for unknown package' do - context 'with an unknown package' do - it 'returns forbidden' do - get api("/packages/npm/unknown") - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - end - - context 'a public project' do - it_behaves_like 'returning the npm package info' - - context 'with application setting enabled' do - before do - stub_application_setting(npm_package_requests_forwarding: true) - end - - it_behaves_like 'returning the npm package info' - - context 'with unknown package' do - subject { get api("/packages/npm/unknown") } - - it 'returns a redirect' do - subject - - expect(response).to have_gitlab_http_status(:found) - expect(response.headers['Location']).to eq('https://registry.npmjs.org/unknown') - end - - it_behaves_like 'a gitlab tracking event', described_class.name, 'npm_request_forward' - end - end - - context 'with application setting disabled' do - before do - stub_application_setting(npm_package_requests_forwarding: false) - end - - it_behaves_like 'returning the npm package info' - - it_behaves_like 'returning forbidden for unknown package' - end - - context 'project path with a dot' do - before do - project.update!(path: 'foo.bar') - end - - it_behaves_like 'returning the npm package info' - end - end - - context 'internal project' do - before do - project.team.truncate - project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) - end - - it_behaves_like 'a package that requires auth' - end - - context 'private project' do - before do - project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - end - - it_behaves_like 'a package that requires auth' - - it 'denies request when not enough permissions' do - project.add_guest(user) - - get_package_with_token(package) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - def get_package(package, params = {}, headers = {}) - get api("/packages/npm/#{package.name}"), params: params, headers: headers - end - - def get_package_with_token(package, params = {}) - get_package(package, params.merge(access_token: token.token)) - end - - def get_package_with_job_token(package, params = {}) - get_package(package, params.merge(job_token: job.token)) - end - - def get_package_with_deploy_token(package, params = {}) - get_package(package, {}, build_token_auth_header(deploy_token.token)) - end - end - - describe 'GET /api/v4/projects/:id/packages/npm/*package_name/-/*file_name' do - let_it_be(:package_file) { package.package_files.first } - - shared_examples 'a package file that requires auth' do - it 'returns the file with an access token' do - get_file_with_token(package_file) - - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq('application/octet-stream') - end - - it 'returns the file with a job token' do - get_file_with_job_token(package_file) - - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq('application/octet-stream') - end - - it 'denies download with no token' do - get_file(package_file) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'a public project' do - subject { get_file(package_file) } - - it 'returns the file with no token needed' do - subject - - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq('application/octet-stream') - end - - it_behaves_like 'a package tracking event', described_class.name, 'pull_package' - end - - context 'private project' do - before do - project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - end - - it_behaves_like 'a package file that requires auth' - - it 'denies download when not enough permissions' do - project.add_guest(user) - - get_file_with_token(package_file) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'internal project' do - before do - project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) - end - - it_behaves_like 'a package file that requires auth' - end - - def get_file(package_file, params = {}) - get api("/projects/#{project.id}/packages/npm/" \ - "#{package_file.package.name}/-/#{package_file.file_name}"), params: params - end - - def get_file_with_token(package_file, params = {}) - get_file(package_file, params.merge(access_token: token.token)) - end - - def get_file_with_job_token(package_file, params = {}) - get_file(package_file, params.merge(job_token: job.token)) - end - end - - describe 'PUT /api/v4/projects/:id/packages/npm/:package_name' do - RSpec.shared_examples 'handling invalid record with 400 error' do - it 'handles an ActiveRecord::RecordInvalid exception with 400 error' do - expect { upload_package_with_token(package_name, params) } - .not_to change { project.packages.count } - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - - context 'when params are correct' do - context 'invalid package record' do - context 'unscoped package' do - let(:package_name) { 'my_unscoped_package' } - let(:params) { upload_params(package_name: package_name) } - - it_behaves_like 'handling invalid record with 400 error' - - context 'with empty versions' do - let(:params) { upload_params(package_name: package_name).merge!(versions: {}) } - - it 'throws a 400 error' do - expect { upload_package_with_token(package_name, params) } - .not_to change { project.packages.count } - - expect(response).to have_gitlab_http_status(:bad_request) - end - end - end - - context 'invalid package name' do - let(:package_name) { "@#{group.path}/my_inv@@lid_package_name" } - let(:params) { upload_params(package_name: package_name) } - - it_behaves_like 'handling invalid record with 400 error' - end - - context 'invalid package version' do - using RSpec::Parameterized::TableSyntax - - let(:package_name) { "@#{group.path}/my_package_name" } - - where(:version) do - [ - '1', - '1.2', - '1./2.3', - '../../../../../1.2.3', - '%2e%2e%2f1.2.3' - ] - end - - with_them do - let(:params) { upload_params(package_name: package_name, package_version: version) } - - it_behaves_like 'handling invalid record with 400 error' - end - end - end - - context 'scoped package' do - let(:package_name) { "@#{group.path}/my_package_name" } - let(:params) { upload_params(package_name: package_name) } - - context 'with access token' do - subject { upload_package_with_token(package_name, params) } - - it_behaves_like 'a package tracking event', described_class.name, 'push_package' - - it 'creates npm package with file' do - expect { subject } - .to change { project.packages.count }.by(1) - .and change { Packages::PackageFile.count }.by(1) - .and change { Packages::Tag.count }.by(1) - - expect(response).to have_gitlab_http_status(:ok) - end - end - - it 'creates npm package with file with job token' do - expect { upload_package_with_job_token(package_name, params) } - .to change { project.packages.count }.by(1) - .and change { Packages::PackageFile.count }.by(1) - - expect(response).to have_gitlab_http_status(:ok) - end - - context 'with an authenticated job token' do - let!(:job) { create(:ci_build, user: user) } - - before do - Grape::Endpoint.before_each do |endpoint| - expect(endpoint).to receive(:current_authenticated_job) { job } - end - end - - after do - Grape::Endpoint.before_each nil - end - - it 'creates the package metadata' do - upload_package_with_token(package_name, params) - - expect(response).to have_gitlab_http_status(:ok) - expect(project.reload.packages.find(json_response['id']).build_info.pipeline).to eq job.pipeline - end - end - end - - context 'package creation fails' do - let(:package_name) { "@#{group.path}/my_package_name" } - let(:params) { upload_params(package_name: package_name) } - - it 'returns an error if the package already exists' do - create(:npm_package, project: project, version: '1.0.1', name: "@#{group.path}/my_package_name") - expect { upload_package_with_token(package_name, params) } - .not_to change { project.packages.count } - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context 'with dependencies' do - let(:package_name) { "@#{group.path}/my_package_name" } - let(:params) { upload_params(package_name: package_name, file: 'npm/payload_with_duplicated_packages.json') } - - it 'creates npm package with file and dependencies' do - expect { upload_package_with_token(package_name, params) } - .to change { project.packages.count }.by(1) - .and change { Packages::PackageFile.count }.by(1) - .and change { Packages::Dependency.count}.by(4) - .and change { Packages::DependencyLink.count}.by(6) - - expect(response).to have_gitlab_http_status(:ok) - end - - context 'with existing dependencies' do - before do - name = "@#{group.path}/existing_package" - upload_package_with_token(name, upload_params(package_name: name, file: 'npm/payload_with_duplicated_packages.json')) - end - - it 'reuses them' do - expect { upload_package_with_token(package_name, params) } - .to change { project.packages.count }.by(1) - .and change { Packages::PackageFile.count }.by(1) - .and not_change { Packages::Dependency.count} - .and change { Packages::DependencyLink.count}.by(6) - end - end - end - end - - def upload_package(package_name, params = {}) - put api("/projects/#{project.id}/packages/npm/#{package_name.sub('/', '%2f')}"), params: params - end - - def upload_package_with_token(package_name, params = {}) - upload_package(package_name, params.merge(access_token: token.token)) - end - - def upload_package_with_job_token(package_name, params = {}) - upload_package(package_name, params.merge(job_token: job.token)) - end - - def upload_params(package_name:, package_version: '1.0.1', file: 'npm/payload.json') - Gitlab::Json.parse(fixture_file("packages/#{file}") - .gsub('@root/npm-test', package_name) - .gsub('1.0.1', package_version)) - end - end - - describe 'GET /api/v4/packages/npm/-/package/*package_name/dist-tags' do - let_it_be(:package_tag1) { create(:packages_tag, package: package) } - let_it_be(:package_tag2) { create(:packages_tag, package: package) } - - let(:package_name) { package.name } - let(:url) { "/packages/npm/-/package/#{package_name}/dist-tags" } - - subject { get api(url) } - - context 'with public project' do - context 'with authenticated user' do - subject { get api(url, personal_access_token: personal_access_token) } - - it_behaves_like 'returns package tags', :maintainer - it_behaves_like 'returns package tags', :developer - it_behaves_like 'returns package tags', :reporter - it_behaves_like 'returns package tags', :guest - end - - context 'with unauthenticated user' do - it_behaves_like 'returns package tags', :no_type - end - end - - context 'with private project' do - before do - project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - end - - context 'with authenticated user' do - subject { get api(url, personal_access_token: personal_access_token) } - - it_behaves_like 'returns package tags', :maintainer - it_behaves_like 'returns package tags', :developer - it_behaves_like 'returns package tags', :reporter - it_behaves_like 'rejects package tags access', :guest, :forbidden - end - - context 'with unauthenticated user' do - it_behaves_like 'rejects package tags access', :no_type, :forbidden - end - end - end - - describe 'PUT /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do - let_it_be(:tag_name) { 'test' } - - let(:package_name) { package.name } - let(:version) { package.version } - let(:url) { "/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}" } - - subject { put api(url), env: { 'api.request.body': version } } - - context 'with public project' do - context 'with authenticated user' do - subject { put api(url, personal_access_token: personal_access_token), env: { 'api.request.body': version } } - - it_behaves_like 'create package tag', :maintainer - it_behaves_like 'create package tag', :developer - it_behaves_like 'rejects package tags access', :reporter, :forbidden - it_behaves_like 'rejects package tags access', :guest, :forbidden - end - - context 'with unauthenticated user' do - it_behaves_like 'rejects package tags access', :no_type, :unauthorized - end - end - - context 'with private project' do - before do - project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - end - - context 'with authenticated user' do - subject { put api(url, personal_access_token: personal_access_token), env: { 'api.request.body': version } } - - it_behaves_like 'create package tag', :maintainer - it_behaves_like 'create package tag', :developer - it_behaves_like 'rejects package tags access', :reporter, :forbidden - it_behaves_like 'rejects package tags access', :guest, :forbidden - end - - context 'with unauthenticated user' do - it_behaves_like 'rejects package tags access', :no_type, :unauthorized - end - end - end - - describe 'DELETE /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do - let_it_be(:package_tag) { create(:packages_tag, package: package) } - - let(:package_name) { package.name } - let(:tag_name) { package_tag.name } - let(:url) { "/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}" } - - subject { delete api(url) } - - context 'with public project' do - context 'with authenticated user' do - subject { delete api(url, personal_access_token: personal_access_token) } - - it_behaves_like 'delete package tag', :maintainer - it_behaves_like 'rejects package tags access', :developer, :forbidden - it_behaves_like 'rejects package tags access', :reporter, :forbidden - it_behaves_like 'rejects package tags access', :guest, :forbidden - end - - context 'with unauthenticated user' do - it_behaves_like 'rejects package tags access', :no_type, :unauthorized - end - end - - context 'with private project' do - before do - project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - end - - context 'with authenticated user' do - subject { delete api(url, personal_access_token: personal_access_token) } - - it_behaves_like 'delete package tag', :maintainer - it_behaves_like 'rejects package tags access', :developer, :forbidden - it_behaves_like 'rejects package tags access', :reporter, :forbidden - it_behaves_like 'rejects package tags access', :guest, :forbidden - end - - context 'with unauthenticated user' do - it_behaves_like 'rejects package tags access', :no_type, :unauthorized - end - end - end - - def expect_a_valid_package_response - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq('application/json') - expect(response).to match_response_schema('public_api/v4/packages/npm_package') - expect(json_response['name']).to eq(package.name) - expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version') - ::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type| - expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any - end - expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags') - end -end diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb new file mode 100644 index 00000000000..1421f20ac28 --- /dev/null +++ b/spec/requests/api/npm_project_packages_spec.rb @@ -0,0 +1,281 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::NpmProjectPackages do + include_context 'npm api setup' + + describe 'GET /api/v4/projects/:id/packages/npm/*package_name' do + it_behaves_like 'handling get metadata requests' do + let(:url) { api("/projects/#{project.id}/packages/npm/#{package_name}") } + end + end + + describe 'GET /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags' do + it_behaves_like 'handling get dist tags requests' do + let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags") } + end + end + + describe 'PUT /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags/:tag' do + it_behaves_like 'handling create dist tag requests' do + let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } + end + end + + describe 'DELETE /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags/:tag' do + it_behaves_like 'handling delete dist tag requests' do + let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") } + end + end + + describe 'GET /api/v4/projects/:id/packages/npm/*package_name/-/*file_name' do + let_it_be(:package_file) { package.package_files.first } + + let(:params) { {} } + let(:url) { api("/projects/#{project.id}/packages/npm/#{package_file.package.name}/-/#{package_file.file_name}") } + + subject { get(url, params: params) } + + shared_examples 'a package file that requires auth' do + it 'denies download with no token' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + + context 'with access token' do + let(:params) { { access_token: token.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 + + context 'with job token' do + let(:params) { { job_token: job.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 + end + + context 'a public project' do + it 'returns the file with no token needed' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/octet-stream') + end + + it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package' + end + + context 'private project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + it_behaves_like 'a package file that requires auth' + + context 'with guest' do + let(:params) { { access_token: token.token } } + + it 'denies download when not enough permissions' do + project.add_guest(user) + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + context 'internal project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + end + + it_behaves_like 'a package file that requires auth' + end + end + + describe 'PUT /api/v4/projects/:id/packages/npm/:package_name' do + RSpec.shared_examples 'handling invalid record with 400 error' do + it 'handles an ActiveRecord::RecordInvalid exception with 400 error' do + expect { upload_package_with_token(package_name, params) } + .not_to change { project.packages.count } + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'when params are correct' do + context 'invalid package record' do + context 'unscoped package' do + let(:package_name) { 'my_unscoped_package' } + let(:params) { upload_params(package_name: package_name) } + + it_behaves_like 'handling invalid record with 400 error' + + context 'with empty versions' do + let(:params) { upload_params(package_name: package_name).merge!(versions: {}) } + + it 'throws a 400 error' do + expect { upload_package_with_token(package_name, params) } + .not_to change { project.packages.count } + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end + + context 'invalid package name' do + let(:package_name) { "@#{group.path}/my_inv@@lid_package_name" } + let(:params) { upload_params(package_name: package_name) } + + it_behaves_like 'handling invalid record with 400 error' + end + + context 'invalid package version' do + using RSpec::Parameterized::TableSyntax + + let(:package_name) { "@#{group.path}/my_package_name" } + + where(:version) do + [ + '1', + '1.2', + '1./2.3', + '../../../../../1.2.3', + '%2e%2e%2f1.2.3' + ] + end + + with_them do + let(:params) { upload_params(package_name: package_name, package_version: version) } + + it_behaves_like 'handling invalid record with 400 error' + end + end + end + + context 'scoped package' do + let(:package_name) { "@#{group.path}/my_package_name" } + let(:params) { upload_params(package_name: package_name) } + + context 'with access token' do + subject { upload_package_with_token(package_name, params) } + + it_behaves_like 'a package tracking event', 'API::NpmPackages', 'push_package' + + it 'creates npm package with file' do + expect { subject } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + .and change { Packages::Tag.count }.by(1) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + it 'creates npm package with file with job token' do + expect { upload_package_with_job_token(package_name, params) } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + + expect(response).to have_gitlab_http_status(:ok) + end + + context 'with an authenticated job token' do + let!(:job) { create(:ci_build, user: user) } + + before do + Grape::Endpoint.before_each do |endpoint| + expect(endpoint).to receive(:current_authenticated_job) { job } + end + end + + after do + Grape::Endpoint.before_each nil + end + + it 'creates the package metadata' do + upload_package_with_token(package_name, params) + + expect(response).to have_gitlab_http_status(:ok) + expect(project.reload.packages.find(json_response['id']).original_build_info.pipeline).to eq job.pipeline + end + end + end + + context 'package creation fails' do + let(:package_name) { "@#{group.path}/my_package_name" } + let(:params) { upload_params(package_name: package_name) } + + it 'returns an error if the package already exists' do + create(:npm_package, project: project, version: '1.0.1', name: "@#{group.path}/my_package_name") + expect { upload_package_with_token(package_name, params) } + .not_to change { project.packages.count } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'with dependencies' do + let(:package_name) { "@#{group.path}/my_package_name" } + let(:params) { upload_params(package_name: package_name, file: 'npm/payload_with_duplicated_packages.json') } + + it 'creates npm package with file and dependencies' do + expect { upload_package_with_token(package_name, params) } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + .and change { Packages::Dependency.count}.by(4) + .and change { Packages::DependencyLink.count}.by(6) + + expect(response).to have_gitlab_http_status(:ok) + end + + context 'with existing dependencies' do + before do + name = "@#{group.path}/existing_package" + upload_package_with_token(name, upload_params(package_name: name, file: 'npm/payload_with_duplicated_packages.json')) + end + + it 'reuses them' do + expect { upload_package_with_token(package_name, params) } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + .and not_change { Packages::Dependency.count} + .and change { Packages::DependencyLink.count}.by(6) + end + end + end + end + + def upload_package(package_name, params = {}) + put api("/projects/#{project.id}/packages/npm/#{package_name.sub('/', '%2f')}"), params: params + end + + def upload_package_with_token(package_name, params = {}) + upload_package(package_name, params.merge(access_token: token.token)) + end + + def upload_package_with_job_token(package_name, params = {}) + upload_package(package_name, params.merge(job_token: job.token)) + end + + def upload_params(package_name:, package_version: '1.0.1', file: 'npm/payload.json') + Gitlab::Json.parse(fixture_file("packages/#{file}") + .gsub('@root/npm-test', package_name) + .gsub('1.0.1', package_version)) + end + end +end diff --git a/spec/requests/api/personal_access_tokens_spec.rb b/spec/requests/api/personal_access_tokens_spec.rb new file mode 100644 index 00000000000..ccc5f322ff9 --- /dev/null +++ b/spec/requests/api/personal_access_tokens_spec.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::PersonalAccessTokens do + let_it_be(:path) { '/personal_access_tokens' } + let_it_be(:token1) { create(:personal_access_token) } + let_it_be(:token2) { create(:personal_access_token) } + let_it_be(:current_user) { create(:user) } + + describe 'GET /personal_access_tokens' do + context 'logged in as an Administrator' do + let_it_be(:current_user) { create(:admin) } + + it 'returns all PATs by default' do + get api(path, current_user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq(PersonalAccessToken.all.count) + end + + context 'filtered with user_id parameter' do + it 'returns only PATs belonging to that user' do + get api(path, current_user), params: { user_id: token1.user.id } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq(1) + expect(json_response.first['user_id']).to eq(token1.user.id) + end + end + + context 'logged in as a non-Administrator' do + let_it_be(:current_user) { create(:user) } + let_it_be(:user) { create(:user) } + let_it_be(:token) { create(:personal_access_token, user: current_user)} + let_it_be(:other_token) { create(:personal_access_token, user: user) } + + it 'returns all PATs belonging to the signed-in user' do + get api(path, current_user, personal_access_token: token) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq(1) + expect(json_response.map { |r| r['user_id'] }.uniq).to contain_exactly(current_user.id) + end + + context 'filtered with user_id parameter' do + it 'returns PATs belonging to the specific user' do + get api(path, current_user, personal_access_token: token), params: { user_id: current_user.id } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq(1) + expect(json_response.map { |r| r['user_id'] }.uniq).to contain_exactly(current_user.id) + end + + it 'is unauthorized if filtered by a user other than current_user' do + get api(path, current_user, personal_access_token: token), params: { user_id: user.id } + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + context 'not authenticated' do + it 'is forbidden' do + get api(path) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + end + + describe 'DELETE /personal_access_tokens/:id' do + let(:path) { "/personal_access_tokens/#{token1.id}" } + + context 'when current_user is an administrator', :enable_admin_mode do + let_it_be(:admin_user) { create(:admin) } + let_it_be(:admin_token) { create(:personal_access_token, user: admin_user) } + let_it_be(:admin_path) { "/personal_access_tokens/#{admin_token.id}" } + + it 'revokes a different users token' do + delete api(path, admin_user) + + expect(response).to have_gitlab_http_status(:no_content) + expect(token1.reload.revoked?).to be true + end + + it 'revokes their own token' do + delete api(admin_path, admin_user) + + expect(response).to have_gitlab_http_status(:no_content) + end + end + + context 'when current_user is not an administrator' do + let_it_be(:user_token) { create(:personal_access_token, user: current_user) } + let_it_be(:user_token_path) { "/personal_access_tokens/#{user_token.id}" } + + it 'fails revokes a different users token' do + delete api(path, current_user) + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'revokes their own token' do + delete api(user_token_path, current_user) + + expect(response).to have_gitlab_http_status(:no_content) + end + end + end +end diff --git a/spec/requests/api/project_container_repositories_spec.rb b/spec/requests/api/project_container_repositories_spec.rb index 34476b10576..15871426ec5 100644 --- a/spec/requests/api/project_container_repositories_spec.rb +++ b/spec/requests/api/project_container_repositories_spec.rb @@ -299,7 +299,7 @@ RSpec.describe API::ProjectContainerRepositories do it_behaves_like 'rejected container repository access', :reporter, :forbidden it_behaves_like 'rejected container repository access', :anonymous, :not_found - context 'for developer' do + context 'for developer', :snowplow do let(:api_user) { developer } context 'when there are multiple tags' do @@ -310,11 +310,11 @@ RSpec.describe API::ProjectContainerRepositories do it 'properly removes tag' do expect(service).to receive(:execute).with(root_repository) { { status: :success } } expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service } - expect(Gitlab::Tracking).to receive(:event).with(described_class.name, 'delete_tag', {}) subject expect(response).to have_gitlab_http_status(:ok) + expect_snowplow_event(category: described_class.name, action: 'delete_tag') end end @@ -326,11 +326,11 @@ RSpec.describe API::ProjectContainerRepositories do it 'properly removes tag' do expect(service).to receive(:execute).with(root_repository) { { status: :success } } expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service } - expect(Gitlab::Tracking).to receive(:event).with(described_class.name, 'delete_tag', {}) subject expect(response).to have_gitlab_http_status(:ok) + expect_snowplow_event(category: described_class.name, action: 'delete_tag') end end end diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb index 09d295afbea..ac24aeee52c 100644 --- a/spec/requests/api/project_export_spec.rb +++ b/spec/requests/api/project_export_spec.rb @@ -26,7 +26,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache do let(:export_path) { "#{Dir.tmpdir}/project_export_spec" } before do - allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) allow_next_instance_of(ProjectExportWorker) do |job| allow(job).to receive(:jid).and_return(SecureRandom.hex(8)) end diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index 3b2a7895630..b5aedde2b2e 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -41,6 +41,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do expect(json_response.first['pipeline_events']).to eq(true) expect(json_response.first['wiki_page_events']).to eq(true) expect(json_response.first['deployment_events']).to eq(true) + expect(json_response.first['releases_events']).to eq(true) expect(json_response.first['enable_ssl_verification']).to eq(true) expect(json_response.first['push_events_branch_filter']).to eq('master') end @@ -72,6 +73,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do expect(json_response['job_events']).to eq(hook.job_events) expect(json_response['pipeline_events']).to eq(hook.pipeline_events) expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) + expect(json_response['releases_events']).to eq(hook.releases_events) expect(json_response['deployment_events']).to eq(true) expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) end @@ -97,7 +99,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do post(api("/projects/#{project.id}/hooks", user), params: { url: "http://example.com", issues_events: true, confidential_issues_events: true, wiki_page_events: true, - job_events: true, deployment_events: true, + job_events: true, deployment_events: true, releases_events: true, push_events_branch_filter: 'some-feature-branch' }) end.to change {project.hooks.count}.by(1) @@ -114,6 +116,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do expect(json_response['pipeline_events']).to eq(false) expect(json_response['wiki_page_events']).to eq(true) expect(json_response['deployment_events']).to eq(true) + expect(json_response['releases_events']).to eq(true) expect(json_response['enable_ssl_verification']).to eq(true) expect(json_response['push_events_branch_filter']).to eq('some-feature-branch') expect(json_response).not_to include('token') @@ -169,6 +172,7 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do expect(json_response['job_events']).to eq(hook.job_events) expect(json_response['pipeline_events']).to eq(hook.pipeline_events) expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) + expect(json_response['releases_events']).to eq(hook.releases_events) expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 2abcb39a1c8..4a792fc218d 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -890,7 +890,7 @@ RSpec.describe API::Projects do expect(response).to have_gitlab_http_status(:created) project.each_pair do |k, v| - next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled storage_version].include?(k) + next if %i[has_external_issue_tracker has_external_wiki issues_enabled merge_requests_enabled wiki_enabled storage_version].include?(k) expect(json_response[k.to_s]).to eq(v) end @@ -1309,7 +1309,7 @@ RSpec.describe API::Projects do expect(response).to have_gitlab_http_status(:created) project.each_pair do |k, v| - next if %i[has_external_issue_tracker path storage_version].include?(k) + next if %i[has_external_issue_tracker has_external_wiki path storage_version].include?(k) expect(json_response[k.to_s]).to eq(v) end @@ -2659,6 +2659,7 @@ RSpec.describe API::Projects do project_param = { container_expiration_policy_attributes: { cadence: '1month', + enabled: true, keep_n: 1, name_regex_keep: '[' } diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb index 82d0d64eba4..c03dd0331cf 100644 --- a/spec/requests/api/release/links_spec.rb +++ b/spec/requests/api/release/links_spec.rb @@ -146,7 +146,7 @@ RSpec.describe API::Release::Links do specify do get api("/projects/#{project.id}/releases/v0.1/assets/links/#{link.id}", maintainer) - expect(json_response['direct_asset_url']).to eq("http://localhost/#{project.namespace.path}/#{project.name}/-/releases/#{release.tag}/bin/bigfile.exe") + expect(json_response['direct_asset_url']).to eq("http://localhost/#{project.namespace.path}/#{project.name}/-/releases/#{release.tag}/downloads/bin/bigfile.exe") end end diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index e78d05835f2..58b321a255e 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -110,22 +110,6 @@ RSpec.describe API::Releases do expect(json_response.second['commit_path']).to eq("/#{release_1.project.full_path}/-/commit/#{release_1.commit.id}") expect(json_response.second['tag_path']).to eq("/#{release_1.project.full_path}/-/tags/#{release_1.tag}") end - - it 'returns the merge requests and issues links, with correct query' do - get api("/projects/#{project.id}/releases", maintainer) - - links = json_response.first['_links'] - release = json_response.first['tag_name'] - expected_query = "release_tag=#{release}&scope=all&state=opened" - path_base = "/#{project.namespace.path}/#{project.path}" - mr_uri = URI.parse(links['merge_requests_url']) - issue_uri = URI.parse(links['issues_url']) - - expect(mr_uri.path).to eq("#{path_base}/-/merge_requests") - expect(issue_uri.path).to eq("#{path_base}/-/issues") - expect(mr_uri.query).to eq(expected_query) - expect(issue_uri.query).to eq(expected_query) - end end it 'returns an upcoming_release status for a future release' do @@ -1014,6 +998,17 @@ RSpec.describe API::Releases do end end + context 'without milestones parameter' do + let(:params) { { name: 'some new name' } } + + it 'does not change the milestone' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(returned_milestones).to match_array(['v1.0']) + end + end + context 'multiple milestones' do context 'with one new' do let!(:milestone2) { create(:milestone, project: project, title: 'milestone2') } diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index 05cfad9cc62..8012892a571 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -23,6 +23,48 @@ RSpec.describe API::Search do end end + shared_examples 'orderable by created_at' do |scope:| + it 'allows ordering results by created_at asc' do + get api(endpoint, user), params: { scope: scope, search: 'sortable', order_by: 'created_at', sort: 'asc' } + + expect(response).to have_gitlab_http_status(:success) + expect(json_response.count).to be > 1 + + created_ats = json_response.map { |r| Time.parse(r['created_at']) } + expect(created_ats.uniq.count).to be > 1 + + expect(created_ats).to eq(created_ats.sort) + end + + it 'allows ordering results by created_at desc' do + get api(endpoint, user), params: { scope: scope, search: 'sortable', order_by: 'created_at', sort: 'desc' } + + expect(response).to have_gitlab_http_status(:success) + expect(json_response.count).to be > 1 + + created_ats = json_response.map { |r| Time.parse(r['created_at']) } + expect(created_ats.uniq.count).to be > 1 + + expect(created_ats).to eq(created_ats.sort.reverse) + end + end + + shared_examples 'issues orderable by created_at' do + before do + create_list(:issue, 3, title: 'sortable item', project: project) + end + + it_behaves_like 'orderable by created_at', scope: :issues + end + + shared_examples 'merge_requests orderable by created_at' do + before do + create_list(:merge_request, 3, :unique_branches, title: 'sortable item', target_project: repo_project, source_project: repo_project) + end + + it_behaves_like 'orderable by created_at', scope: :merge_requests + end + shared_examples 'pagination' do |scope:, search: ''| it 'returns a different result for each page' do get api(endpoint, user), params: { scope: scope, search: search, page: 1, per_page: 1 } @@ -121,6 +163,8 @@ RSpec.describe API::Search do it_behaves_like 'ping counters', scope: :issues + it_behaves_like 'issues orderable by created_at' + describe 'pagination' do before do create(:issue, project: project, title: 'another issue') @@ -151,7 +195,6 @@ RSpec.describe API::Search do context 'filter by confidentiality' do before do - stub_feature_flags(search_filter_by_confidential: true) create(:issue, project: project, author: user, title: 'awesome non-confidential issue') create(:issue, :confidential, project: project, author: user, title: 'awesome confidential issue') end @@ -182,6 +225,8 @@ RSpec.describe API::Search do it_behaves_like 'ping counters', scope: :merge_requests + it_behaves_like 'merge_requests orderable by created_at' + describe 'pagination' do before do create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch') @@ -355,6 +400,8 @@ RSpec.describe API::Search do it_behaves_like 'ping counters', scope: :issues + it_behaves_like 'issues orderable by created_at' + describe 'pagination' do before do create(:issue, project: project, title: 'another issue') @@ -375,6 +422,8 @@ RSpec.describe API::Search do it_behaves_like 'ping counters', scope: :merge_requests + it_behaves_like 'merge_requests orderable by created_at' + describe 'pagination' do before do create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch') @@ -507,6 +556,8 @@ RSpec.describe API::Search do it_behaves_like 'ping counters', scope: :issues + it_behaves_like 'issues orderable by created_at' + describe 'pagination' do before do create(:issue, project: project, title: 'another issue') @@ -516,6 +567,14 @@ RSpec.describe API::Search do end end + context 'when requesting basic search' do + it 'passes the parameter to search service' do + expect(SearchService).to receive(:new).with(user, hash_including(basic_search: 'true')) + + get api(endpoint, user), params: { scope: 'issues', search: 'awesome', basic_search: 'true' } + end + end + context 'for merge_requests scope' do let(:endpoint) { "/projects/#{repo_project.id}/search" } @@ -529,6 +588,8 @@ RSpec.describe API::Search do it_behaves_like 'ping counters', scope: :merge_requests + it_behaves_like 'merge_requests orderable by created_at' + describe 'pagination' do before do create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch') diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 8b5f74df8f8..03320549e44 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -22,6 +22,8 @@ RSpec.describe API::Settings, 'Settings' do expect(json_response['default_ci_config_path']).to be_nil expect(json_response['sourcegraph_enabled']).to be_falsey expect(json_response['sourcegraph_url']).to be_nil + expect(json_response['secret_detection_token_revocation_url']).to be_nil + expect(json_response['secret_detection_revocation_token_types_url']).to be_nil expect(json_response['sourcegraph_public_only']).to be_truthy expect(json_response['default_project_visibility']).to be_a String expect(json_response['default_snippet_visibility']).to be_a String @@ -40,6 +42,7 @@ RSpec.describe API::Settings, 'Settings' do 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) + expect(json_response['require_admin_approval_after_user_signup']).to eq(true) end end @@ -105,7 +108,7 @@ RSpec.describe API::Settings, 'Settings' do enforce_terms: true, terms: 'Hello world!', performance_bar_allowed_group_path: group.full_path, - diff_max_patch_bytes: 150_000, + diff_max_patch_bytes: 300_000, default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE, local_markdown_version: 3, allow_local_requests_from_web_hooks_and_services: true, @@ -148,7 +151,7 @@ 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['diff_max_patch_bytes']).to eq(150_000) + expect(json_response['diff_max_patch_bytes']).to eq(300_000) expect(json_response['default_branch_protection']).to eq(Gitlab::Access::PROTECTION_DEV_CAN_MERGE) expect(json_response['local_markdown_version']).to eq(3) expect(json_response['allow_local_requests_from_web_hooks_and_services']).to eq(true) @@ -377,41 +380,41 @@ RSpec.describe API::Settings, 'Settings' do end end - context 'domain_blacklist settings' do - it 'rejects domain_blacklist_enabled when domain_blacklist is empty' do + context 'domain_denylist settings' do + it 'rejects domain_denylist_enabled when domain_denylist is empty' do put api('/application/settings', admin), params: { - domain_blacklist_enabled: true, - domain_blacklist: [] + domain_denylist_enabled: true, + domain_denylist: [] } expect(response).to have_gitlab_http_status(:bad_request) message = json_response["message"] - expect(message["domain_blacklist"]).to eq(["Domain blacklist cannot be empty if Blacklist is enabled."]) + expect(message["domain_denylist"]).to eq(["Domain denylist cannot be empty if denylist is enabled."]) end - it 'allows array for domain_blacklist' do + it 'allows array for domain_denylist' do put api('/application/settings', admin), params: { - domain_blacklist_enabled: true, - domain_blacklist: ['domain1.com', 'domain2.com'] + domain_denylist_enabled: true, + domain_denylist: ['domain1.com', 'domain2.com'] } expect(response).to have_gitlab_http_status(:ok) - expect(json_response['domain_blacklist_enabled']).to be(true) - expect(json_response['domain_blacklist']).to eq(['domain1.com', 'domain2.com']) + expect(json_response['domain_denylist_enabled']).to be(true) + expect(json_response['domain_denylist']).to eq(['domain1.com', 'domain2.com']) end - it 'allows a string for domain_blacklist' do + it 'allows a string for domain_denylist' do put api('/application/settings', admin), params: { - domain_blacklist_enabled: true, - domain_blacklist: 'domain3.com, *.domain4.com' + domain_denylist_enabled: true, + domain_denylist: 'domain3.com, *.domain4.com' } expect(response).to have_gitlab_http_status(:ok) - expect(json_response['domain_blacklist_enabled']).to be(true) - expect(json_response['domain_blacklist']).to eq(['domain3.com', '*.domain4.com']) + expect(json_response['domain_denylist_enabled']).to be(true) + expect(json_response['domain_denylist']).to eq(['domain3.com', '*.domain4.com']) end end @@ -423,6 +426,14 @@ RSpec.describe API::Settings, 'Settings' do expect(json_response['abuse_notification_email']).to eq('test@example.com') end + it 'supports setting require_admin_approval_after_user_signup' do + put api('/application/settings', admin), + params: { require_admin_approval_after_user_signup: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['require_admin_approval_after_user_signup']).to eq(true) + end + context "missing sourcegraph_url value when sourcegraph_enabled is true" do it "returns a blank parameter error message" do put api("/application/settings", admin), params: { sourcegraph_enabled: true } diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb index b91f6e1aa88..0fa088a641e 100644 --- a/spec/requests/api/terraform/state_spec.rb +++ b/spec/requests/api/terraform/state_spec.rb @@ -113,7 +113,7 @@ RSpec.describe API::Terraform::State do end describe 'POST /projects/:id/terraform/state/:name' do - let(:params) { { 'instance': 'example-instance', 'serial': '1' } } + let(:params) { { 'instance': 'example-instance', 'serial': state.latest_version.version + 1 } } subject(:request) { post api(state_path), headers: auth_header, as: :json, params: params } @@ -202,6 +202,18 @@ RSpec.describe API::Terraform::State do end end end + + context 'when using job token authentication' do + let(:job) { create(:ci_build, status: :running, project: project, user: maintainer) } + let(:auth_header) { job_basic_auth_header(job) } + + it 'associates the job with the newly created state version' do + expect { request }.to change { state.versions.count }.by(1) + + expect(response).to have_gitlab_http_status(:ok) + expect(state.reload_latest_version.build).to eq(job) + end + end end describe 'DELETE /projects/:id/terraform/state/:name' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 7330c89fe77..98840d6238a 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -161,7 +161,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do end context 'accesses the profile of another admin' do - let(:admin_2) {create(:admin, note: '2010-10-10 | 2FA added | admin requested | www.gitlab.com')} + let(:admin_2) { create(:admin, note: '2010-10-10 | 2FA added | admin requested | www.gitlab.com') } it 'contains the note of the user' do get api("/user?private_token=#{admin_personal_access_token}&sudo=#{admin_2.id}") @@ -772,11 +772,11 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do it "does not create user with invalid email" do post api('/users', admin), - params: { - email: 'invalid email', - password: 'password', - name: 'test' - } + params: { + email: 'invalid email', + password: 'password', + name: 'test' + } expect(response).to have_gitlab_http_status(:bad_request) end @@ -811,14 +811,14 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do it 'returns 400 error if user does not validate' do post api('/users', admin), - params: { - password: 'pass', - email: 'test@example.com', - username: 'test!', - name: 'test', - bio: 'g' * 256, - projects_limit: -1 - } + params: { + password: 'pass', + email: 'test@example.com', + username: 'test!', + name: 'test', + bio: 'g' * 256, + projects_limit: -1 + } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']['password']) .to eq(['is too short (minimum is 8 characters)']) @@ -838,23 +838,23 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do context 'with existing user' do before do post api('/users', admin), - params: { - email: 'test@example.com', - password: 'password', - username: 'test', - name: 'foo' - } + params: { + email: 'test@example.com', + password: 'password', + username: 'test', + name: 'foo' + } end it 'returns 409 conflict error if user with same email exists' do expect do post api('/users', admin), - params: { - name: 'foo', - email: 'test@example.com', - password: 'password', - username: 'foo' - } + params: { + name: 'foo', + email: 'test@example.com', + password: 'password', + username: 'foo' + } end.to change { User.count }.by(0) expect(response).to have_gitlab_http_status(:conflict) expect(json_response['message']).to eq('Email has already been taken') @@ -863,12 +863,12 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do it 'returns 409 conflict error if same username exists' do expect do post api('/users', admin), - params: { - name: 'foo', - email: 'foo@example.com', - password: 'password', - username: 'test' - } + params: { + name: 'foo', + email: 'foo@example.com', + password: 'password', + username: 'test' + } end.to change { User.count }.by(0) expect(response).to have_gitlab_http_status(:conflict) expect(json_response['message']).to eq('Username has already been taken') @@ -877,12 +877,12 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do it 'returns 409 conflict error if same username exists (case insensitive)' do expect do post api('/users', admin), - params: { - name: 'foo', - email: 'foo@example.com', - password: 'password', - username: 'TEST' - } + params: { + name: 'foo', + email: 'foo@example.com', + password: 'password', + username: 'TEST' + } end.to change { User.count }.by(0) expect(response).to have_gitlab_http_status(:conflict) expect(json_response['message']).to eq('Username has already been taken') @@ -1185,14 +1185,14 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do it 'returns 400 error if user does not validate' do put api("/users/#{user.id}", admin), - params: { - password: 'pass', - email: 'test@example.com', - username: 'test!', - name: 'test', - bio: 'g' * 256, - projects_limit: -1 - } + params: { + password: 'pass', + email: 'test@example.com', + username: 'test!', + name: 'test', + bio: 'g' * 256, + projects_limit: -1 + } expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['message']['password']) .to eq(['is too short (minimum is 8 characters)']) @@ -1714,14 +1714,14 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do context "hard delete disabled" do it "does not delete user" do - perform_enqueued_jobs { delete api("/users/#{user.id}", admin)} + perform_enqueued_jobs { delete api("/users/#{user.id}", admin) } expect(response).to have_gitlab_http_status(:conflict) end end context "hard delete enabled" do it "delete user and group", :sidekiq_might_not_need_inline do - perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin)} + perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) } expect(response).to have_gitlab_http_status(:no_content) expect(Group.exists?(group.id)).to be_falsy end @@ -1993,7 +1993,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do delete api("/user/keys/#{key.id}", user) expect(response).to have_gitlab_http_status(:no_content) - end.to change { user.keys.count}.by(-1) + end.to change { user.keys.count }.by(-1) end it_behaves_like '412 response' do @@ -2124,7 +2124,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do post api("/user/gpg_keys/#{gpg_key.id}/revoke", user) expect(response).to have_gitlab_http_status(:accepted) - end.to change { user.gpg_keys.count}.by(-1) + end.to change { user.gpg_keys.count }.by(-1) end it 'returns 404 if key ID not found' do @@ -2157,7 +2157,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do delete api("/user/gpg_keys/#{gpg_key.id}", user) expect(response).to have_gitlab_http_status(:no_content) - end.to change { user.gpg_keys.count}.by(-1) + end.to change { user.gpg_keys.count }.by(-1) end it 'returns 404 if key ID not found' do @@ -2279,7 +2279,7 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do delete api("/user/emails/#{email.id}", user) expect(response).to have_gitlab_http_status(:no_content) - end.to change { user.emails.count}.by(-1) + end.to change { user.emails.count }.by(-1) end it_behaves_like '412 response' do @@ -2756,6 +2756,124 @@ RSpec.describe API::Users, :do_not_mock_admin_mode do end end + describe 'POST /users/:user_id/personal_access_tokens' do + let(:name) { 'new pat' } + let(:expires_at) { 3.days.from_now.to_date.to_s } + let(:scopes) { %w(api read_user) } + + context 'when feature flag is enabled' do + before do + stub_feature_flags(pat_creation_api_for_admin: true) + end + + it 'returns error if required attributes are missing' do + post api("/users/#{user.id}/personal_access_tokens", admin) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq('name is missing, scopes is missing, scopes does not have a valid value') + end + + it 'returns a 404 error if user not found' do + post api("/users/#{non_existing_record_id}/personal_access_tokens", admin), + params: { + name: name, + scopes: scopes, + expires_at: expires_at + } + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns a 401 error when not authenticated' do + post api("/users/#{user.id}/personal_access_tokens"), + params: { + name: name, + scopes: scopes, + expires_at: expires_at + } + + expect(response).to have_gitlab_http_status(:unauthorized) + expect(json_response['message']).to eq('401 Unauthorized') + end + + it 'returns a 403 error when authenticated as normal user' do + post api("/users/#{user.id}/personal_access_tokens", user), + params: { + name: name, + scopes: scopes, + expires_at: expires_at + } + + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['message']).to eq('403 Forbidden') + end + + it 'creates a personal access token when authenticated as admin' do + post api("/users/#{user.id}/personal_access_tokens", admin), + params: { + name: name, + expires_at: expires_at, + scopes: scopes + } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['name']).to eq(name) + expect(json_response['scopes']).to eq(scopes) + expect(json_response['expires_at']).to eq(expires_at) + expect(json_response['id']).to be_present + expect(json_response['created_at']).to be_present + expect(json_response['active']).to be_truthy + expect(json_response['revoked']).to be_falsey + expect(json_response['token']).to be_present + end + + context 'when an error is thrown by the model' do + let!(:admin_personal_access_token) { create(:personal_access_token, user: admin) } + let(:error_message) { 'error message' } + + before do + allow_next_instance_of(PersonalAccessToken) do |personal_access_token| + allow(personal_access_token).to receive_message_chain(:errors, :full_messages) + .and_return([error_message]) + + allow(personal_access_token).to receive(:save).and_return(false) + end + end + + it 'returns the error' do + post api("/users/#{user.id}/personal_access_tokens", personal_access_token: admin_personal_access_token), + params: { + name: name, + expires_at: expires_at, + scopes: scopes + } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response['message']).to eq(error_message) + end + end + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(pat_creation_api_for_admin: false) + end + + it 'returns a 404' do + post api("/users/#{user.id}/personal_access_tokens", admin), + params: { + name: name, + expires_at: expires_at, + scopes: scopes + } + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Not Found') + end + end + end + describe 'GET /users/:user_id/impersonation_tokens' do let_it_be(:active_personal_access_token) { create(:personal_access_token, user: user) } let_it_be(:revoked_personal_access_token) { create(:personal_access_token, :revoked, user: user) } |