From b595cb0c1dec83de5bdee18284abe86614bed33b Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 20 Jul 2022 15:40:28 +0000 Subject: Add latest changes from gitlab-org/gitlab@15-2-stable-ee --- spec/requests/api/api_spec.rb | 4 +- spec/requests/api/award_emoji_spec.rb | 112 ++++++--- .../api/ci/runner/jobs_request_post_spec.rb | 73 ++++-- spec/requests/api/commits_spec.rb | 19 +- spec/requests/api/conan_instance_packages_spec.rb | 6 + spec/requests/api/conan_project_packages_spec.rb | 6 + spec/requests/api/environments_spec.rb | 83 ------- spec/requests/api/events_spec.rb | 2 +- spec/requests/api/feature_flags_user_lists_spec.rb | 7 + spec/requests/api/geo_spec.rb | 14 +- .../api/graphql/boards/board_lists_query_spec.rb | 3 +- .../api/graphql/ci/group_variables_spec.rb | 67 ++++++ .../api/graphql/ci/instance_variables_spec.rb | 60 +++++ spec/requests/api/graphql/ci/job_spec.rb | 4 +- spec/requests/api/graphql/ci/jobs_spec.rb | 6 +- .../api/graphql/ci/manual_variables_spec.rb | 95 ++++++++ spec/requests/api/graphql/ci/pipelines_spec.rb | 4 +- .../api/graphql/ci/project_variables_spec.rb | 67 ++++++ spec/requests/api/graphql/ci/runner_spec.rb | 2 +- spec/requests/api/graphql/ci/stages_spec.rb | 2 +- .../container_repository_details_spec.rb | 2 +- spec/requests/api/graphql/crm/contacts_spec.rb | 69 ++++++ .../api/graphql/current_user/groups_query_spec.rb | 34 ++- .../graphql/group/container_repositories_spec.rb | 2 +- .../graphql/group/dependency_proxy_blobs_spec.rb | 2 +- .../group/dependency_proxy_group_setting_spec.rb | 2 +- .../dependency_proxy_image_ttl_policy_spec.rb | 2 +- .../group/dependency_proxy_manifests_spec.rb | 7 +- .../api/graphql/group/group_members_spec.rb | 48 +++- .../api/graphql/mutations/issues/create_spec.rb | 36 +++ .../mutations/notes/create/diff_note_spec.rb | 14 +- .../api/graphql/mutations/snippets/update_spec.rb | 17 +- .../mutations/work_items/create_from_task_spec.rb | 1 + .../graphql/mutations/work_items/create_spec.rb | 89 +++++++ .../mutations/work_items/delete_task_spec.rb | 2 +- .../graphql/mutations/work_items/update_spec.rb | 268 ++++++++++++++++++++- .../mutations/work_items/update_widgets_spec.rb | 51 ++-- .../graphql/project/container_repositories_spec.rb | 2 +- spec/requests/api/graphql/project/issues_spec.rb | 37 ++- spec/requests/api/graphql/project/jobs_spec.rb | 4 +- .../project/packages_cleanup_policy_spec.rb | 2 +- spec/requests/api/graphql/project/pipeline_spec.rb | 60 ++++- .../api/graphql/project/project_members_spec.rb | 48 +++- spec/requests/api/graphql/todo_query_spec.rb | 50 ++++ spec/requests/api/graphql/work_item_spec.rb | 98 ++++++-- spec/requests/api/group_export_spec.rb | 14 +- spec/requests/api/group_variables_spec.rb | 2 +- spec/requests/api/groups_spec.rb | 14 +- spec/requests/api/integrations_spec.rb | 40 ++- spec/requests/api/internal/base_spec.rb | 58 ----- spec/requests/api/internal/error_tracking_spec.rb | 108 +++++++++ spec/requests/api/internal/kubernetes_spec.rb | 4 +- spec/requests/api/invitations_spec.rb | 40 ++- spec/requests/api/issues/issues_spec.rb | 14 +- spec/requests/api/markdown_snapshot_spec.rb | 4 +- spec/requests/api/maven_packages_spec.rb | 40 ++- spec/requests/api/metadata_spec.rb | 94 ++++++++ spec/requests/api/npm_project_packages_spec.rb | 14 +- spec/requests/api/project_attributes.yml | 6 + spec/requests/api/project_export_spec.rb | 14 +- spec/requests/api/project_hooks_spec.rb | 247 +++---------------- spec/requests/api/project_import_spec.rb | 10 + spec/requests/api/projects_spec.rb | 43 +++- spec/requests/api/protected_tags_spec.rb | 7 +- spec/requests/api/pypi_packages_spec.rb | 15 +- spec/requests/api/repositories_spec.rb | 74 ++++++ spec/requests/api/settings_spec.rb | 2 +- spec/requests/api/snippets_spec.rb | 38 ++- spec/requests/api/system_hooks_spec.rb | 229 +++--------------- spec/requests/api/tags_spec.rb | 7 + .../api/terraform/modules/v1/packages_spec.rb | 210 ++++++++++++++++ spec/requests/api/unleash_spec.rb | 42 +++- spec/requests/api/users_spec.rb | 109 +++++++++ 73 files changed, 2310 insertions(+), 772 deletions(-) create mode 100644 spec/requests/api/graphql/ci/group_variables_spec.rb create mode 100644 spec/requests/api/graphql/ci/instance_variables_spec.rb create mode 100644 spec/requests/api/graphql/ci/manual_variables_spec.rb create mode 100644 spec/requests/api/graphql/ci/project_variables_spec.rb create mode 100644 spec/requests/api/graphql/crm/contacts_spec.rb create mode 100644 spec/requests/api/graphql/todo_query_spec.rb create mode 100644 spec/requests/api/internal/error_tracking_spec.rb create mode 100644 spec/requests/api/metadata_spec.rb (limited to 'spec/requests/api') diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb index df9be2616c5..b6cb790bb71 100644 --- a/spec/requests/api/api_spec.rb +++ b/spec/requests/api/api_spec.rb @@ -133,7 +133,7 @@ RSpec.describe API::API do 'meta.caller_id' => 'GET /api/:version/broadcast_messages', 'meta.remote_ip' => an_instance_of(String), 'meta.client_id' => a_string_matching(%r{\Aip/.+}), - 'meta.feature_category' => 'navigation', + 'meta.feature_category' => 'onboarding', 'route' => '/api/:version/broadcast_messages') expect(data.stringify_keys).not_to include('meta.project', 'meta.root_namespace', 'meta.user') @@ -209,7 +209,7 @@ RSpec.describe API::API do 'meta.caller_id' => 'GET /api/:version/broadcast_messages', 'meta.remote_ip' => an_instance_of(String), 'meta.client_id' => a_string_matching(%r{\Aip/.+}), - 'meta.feature_category' => 'navigation', + 'meta.feature_category' => 'onboarding', 'route' => '/api/:version/broadcast_messages') expect(data.stringify_keys).not_to include('meta.project', 'meta.root_namespace', 'meta.user') diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index 782e14593f7..67ddaf2fda5 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' RSpec.describe API::AwardEmoji do + let_it_be_with_reload(:project) { create(:project, :private) } let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project) } let_it_be(:issue) { create(:issue, project: project) } let_it_be(:award_emoji) { create(:award_emoji, awardable: issue, user: user) } let_it_be(:note) { create(:note, project: project, noteable: issue) } @@ -16,10 +16,46 @@ RSpec.describe API::AwardEmoji do project.add_maintainer(user) end + shared_examples 'request with insufficient permissions' do |request_method| + let(:request_params) { {} } + + context 'when user is not signed in' do + it 'returns 404' do + process request_method, api(request_path), params: request_params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when user does not have access' do + it 'returns 404' do + other_user = create(:user) + + process request_method, api(request_path, other_user), params: request_params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + shared_examples 'unauthenticated request to public awardable' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end + + it 'returns the awarded emoji' do + get api(request_path) + + expect(response).to have_gitlab_http_status(:ok) + end + end + describe "GET /projects/:id/awardable/:awardable_id/award_emoji" do context 'on an issue' do + let(:request_path) { "/projects/#{project.id}/issues/#{issue.iid}/award_emoji" } + it "returns an array of award_emoji" do - get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user) + get api(request_path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array @@ -48,6 +84,9 @@ RSpec.describe API::AwardEmoji do expect(response).to have_gitlab_http_status(:not_found) end + + it_behaves_like 'unauthenticated request to public awardable' + it_behaves_like 'request with insufficient permissions', :get end context 'on a merge request' do @@ -73,34 +112,30 @@ RSpec.describe API::AwardEmoji do expect(json_response.first['name']).to eq(award.name) end end - - context 'when the user has no access' do - it 'returns a status code 404' do - user1 = create(:user) - - get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji", user1) - - expect(response).to have_gitlab_http_status(:not_found) - end - end end describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji' do - let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') } + let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') } + let(:request_path) { "/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji" } it 'returns an array of award emoji' do - get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user) + get api(request_path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array expect(json_response.first['name']).to eq(rocket.name) end + + it_behaves_like 'unauthenticated request to public awardable' + it_behaves_like 'request with insufficient permissions', :get end describe "GET /projects/:id/awardable/:awardable_id/award_emoji/:award_id" do context 'on an issue' do + let(:request_path) { "/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}" } + it "returns the award emoji" do - get api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}", user) + get api(request_path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response['name']).to eq(award_emoji.name) @@ -113,6 +148,9 @@ RSpec.describe API::AwardEmoji do expect(response).to have_gitlab_http_status(:not_found) end + + it_behaves_like 'unauthenticated request to public awardable' + it_behaves_like 'request with insufficient permissions', :get end context 'on a merge request' do @@ -139,28 +177,22 @@ RSpec.describe API::AwardEmoji do expect(json_response['awardable_type']).to eq("Snippet") end end - - context 'when the user has no access' do - it 'returns a status code 404' do - user1 = create(:user) - - get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user1) - - expect(response).to have_gitlab_http_status(:not_found) - end - end end describe 'GET /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji/:award_id' do - let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') } + let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket') } + let(:request_path) { "/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}" } it 'returns an award emoji' do - get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}", user) + get api(request_path, user) expect(response).to have_gitlab_http_status(:ok) expect(json_response).not_to be_an Array expect(json_response['name']).to eq(rocket.name) end + + it_behaves_like 'unauthenticated request to public awardable' + it_behaves_like 'request with insufficient permissions', :get end describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do @@ -189,12 +221,6 @@ RSpec.describe API::AwardEmoji do expect(response).to have_gitlab_http_status(:bad_request) end - it "returns a 401 unauthorized error if the user is not authenticated" do - post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji"), params: { name: 'thumbsup' } - - expect(response).to have_gitlab_http_status(:unauthorized) - end - it "normalizes +1 as thumbsup award" do post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), params: { name: '+1' } @@ -223,6 +249,11 @@ RSpec.describe API::AwardEmoji do expect(json_response['user']['username']).to eq(user.username) end end + + it_behaves_like 'request with insufficient permissions', :post do + let(:request_path) { "/projects/#{project.id}/issues/#{issue.iid}/award_emoji" } + let(:request_params) { { name: 'blowfish' } } + end end describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do @@ -260,6 +291,11 @@ RSpec.describe API::AwardEmoji do expect(json_response["message"]).to match("has already been taken") end end + + it_behaves_like 'request with insufficient permissions', :post do + let(:request_path) { "/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji" } + let(:request_params) { { name: 'rocket' } } + end end describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_id' do @@ -319,9 +355,13 @@ RSpec.describe API::AwardEmoji do let(:request) { api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user) } end end + + it_behaves_like 'request with insufficient permissions', :delete do + let(:request_path) { "/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}" } + end end - describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do + describe 'DELETE /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji/:award_id' do let!(:rocket) { create(:award_emoji, awardable: note, name: 'rocket', user: user) } it 'deletes the award' do @@ -335,5 +375,9 @@ RSpec.describe API::AwardEmoji do it_behaves_like '412 response' do let(:request) { api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}", user) } end + + it_behaves_like 'request with insufficient permissions', :delete do + let(:request_path) { "/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}" } + end end end diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb index 3c6f9ac2816..746be1ccc44 100644 --- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb +++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb @@ -216,13 +216,17 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do expect(json_response['token']).to eq(job.token) expect(json_response['job_info']).to eq(expected_job_info) expect(json_response['git_info']).to eq(expected_git_info) - expect(json_response['image']).to eq({ 'name' => 'image:1.0', 'entrypoint' => '/bin/sh', 'ports' => [], 'pull_policy' => nil }) - expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil, - 'alias' => nil, 'command' => nil, 'ports' => [], 'variables' => nil }, - { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh', - 'alias' => 'docker', 'command' => 'sleep 30', 'ports' => [], 'variables' => [] }, - { 'name' => 'mysql:latest', 'entrypoint' => nil, - 'alias' => nil, 'command' => nil, 'ports' => [], 'variables' => [{ 'key' => 'MYSQL_ROOT_PASSWORD', 'value' => 'root123.' }] }]) + expect(json_response['image']).to eq( + { 'name' => 'image:1.0', 'entrypoint' => '/bin/sh', 'ports' => [], 'pull_policy' => nil } + ) + expect(json_response['services']).to eq([ + { 'name' => 'postgres', 'entrypoint' => nil, 'alias' => nil, 'command' => nil, 'ports' => [], + 'variables' => nil, 'pull_policy' => nil }, + { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh', 'alias' => 'docker', 'command' => 'sleep 30', + 'ports' => [], 'variables' => [], 'pull_policy' => nil }, + { 'name' => 'mysql:latest', 'entrypoint' => nil, 'alias' => nil, 'command' => nil, 'ports' => [], + 'variables' => [{ 'key' => 'MYSQL_ROOT_PASSWORD', 'value' => 'root123.' }], 'pull_policy' => nil } + ]) expect(json_response['steps']).to eq(expected_steps) expect(json_response['artifacts']).to eq(expected_artifacts) expect(json_response['cache']).to match(expected_cache) @@ -542,7 +546,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } let!(:test_job) do - create(:ci_build, :pending, :queued, pipeline: pipeline, token: 'test-job-token', name: 'deploy', + create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1, options: { script: ['bash'], dependencies: [job2.name] }) end @@ -566,7 +570,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } let!(:empty_dependencies_job) do - create(:ci_build, :pending, :queued, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job', + create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'empty_dependencies_job', stage: 'deploy', stage_idx: 1, options: { script: ['bash'], dependencies: [] }) end @@ -671,14 +675,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end end - context 'when variables are stored in trigger_request' do - before do - trigger_request.update_attribute(:variables, { TRIGGER_KEY_1: 'TRIGGER_VALUE_1' } ) - end - - it_behaves_like 'expected variables behavior' - end - context 'when variables are stored in pipeline_variables' do before do create(:ci_pipeline_variable, pipeline: pipeline, key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1') @@ -849,10 +845,51 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end end + context 'when service has pull_policy' do + let(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) } + + let(:options) do + { + services: [{ + name: 'postgres:11.9', + pull_policy: ['if-not-present'] + }] + } + end + + it 'returns the service with pull policy' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to include( + 'id' => job.id, + 'services' => [{ 'alias' => nil, 'command' => nil, 'entrypoint' => nil, 'name' => 'postgres:11.9', + 'ports' => [], 'pull_policy' => ['if-not-present'], 'variables' => [] }] + ) + end + + context 'when the FF ci_docker_image_pull_policy is disabled' do + before do + stub_feature_flags(ci_docker_image_pull_policy: false) + end + + it 'returns the service without pull policy' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to include( + 'id' => job.id, + 'services' => [{ 'alias' => nil, 'command' => nil, 'entrypoint' => nil, 'name' => 'postgres:11.9', + 'ports' => [], 'variables' => [] }] + ) + end + end + end + describe 'a job with excluded artifacts' do context 'when excluded paths are defined' do let(:job) do - create(:ci_build, :pending, :queued, pipeline: pipeline, token: 'test-job-token', name: 'test', + create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'test', stage: 'deploy', stage_idx: 1, options: { artifacts: { paths: ['abc'], exclude: ['cde'] } }) end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 67c2ec91540..9ef845f06bf 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -392,14 +392,25 @@ RSpec.describe API::Commits do end end - context 'when using warden' do - it 'increments usage counters', :clean_gitlab_redis_sessions do - stub_session('warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]]) + context 'when using warden', :snowplow, :clean_gitlab_redis_sessions do + before do + stub_session('warden.user.user.key' => [[user.id], user.authenticatable_salt]) + end + + subject { post api(url), params: valid_c_params } + it 'increments usage counters' do expect(::Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_commits_count) expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_web_ide_edit_action) - post api(url), params: valid_c_params + subject + end + + it_behaves_like 'Snowplow event tracking' do + let(:namespace) { project.namespace } + let(:category) { 'ide_edit' } + let(:action) { 'g_edit_by_web_ide' } + let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } end end diff --git a/spec/requests/api/conan_instance_packages_spec.rb b/spec/requests/api/conan_instance_packages_spec.rb index ff3b332c620..54cad3093d7 100644 --- a/spec/requests/api/conan_instance_packages_spec.rb +++ b/spec/requests/api/conan_instance_packages_spec.rb @@ -17,6 +17,12 @@ RSpec.describe API::ConanInstancePackages do let_it_be(:url) { '/packages/conan/v1/conans/search' } it_behaves_like 'conan search endpoint' + + it_behaves_like 'conan FIPS mode' do + let(:params) { { q: package.conan_recipe } } + + subject { get api(url), params: params } + end end describe 'GET /api/v4/packages/conan/v1/users/authenticate' do diff --git a/spec/requests/api/conan_project_packages_spec.rb b/spec/requests/api/conan_project_packages_spec.rb index c108f2efaaf..e28105eb8eb 100644 --- a/spec/requests/api/conan_project_packages_spec.rb +++ b/spec/requests/api/conan_project_packages_spec.rb @@ -17,6 +17,12 @@ RSpec.describe API::ConanProjectPackages do let(:url) { "/projects/#{project.id}/packages/conan/v1/conans/search" } it_behaves_like 'conan search endpoint' + + it_behaves_like 'conan FIPS mode' do + let(:params) { { q: package.conan_recipe } } + + subject { get api(url), params: params } + end end describe 'GET /api/v4/projects/:id/packages/conan/v1/users/authenticate' do diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 93f21c880a4..a35c1630caa 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -26,90 +26,7 @@ RSpec.describe API::Environments do expect(json_response.first['tier']).to eq(environment.tier) expect(json_response.first['external_url']).to eq(environment.external_url) expect(json_response.first['project']).to match_schema('public_api/v4/project') - expect(json_response.first['enable_advanced_logs_querying']).to eq(false) expect(json_response.first).not_to have_key('last_deployment') - expect(json_response.first).not_to have_key('gitlab_managed_apps_logs_path') - end - - context 'when the user can read pod logs' do - context 'with successful deployment on cluster' do - let_it_be(:deployment) { create(:deployment, :on_cluster, :success, environment: environment, project: project) } - - it 'returns environment with enable_advanced_logs_querying and logs_api_path' do - get api("/projects/#{project.id}/environments", 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(1) - expect(json_response.first['gitlab_managed_apps_logs_path']).to eq( - "/#{project.full_path}/-/logs/k8s.json?cluster_id=#{deployment.cluster_id}" - ) - end - end - - context 'when elastic stack is available' do - before do - allow_next_found_instance_of(Environment) do |env| - allow(env).to receive(:elastic_stack_available?).and_return(true) - end - end - - it 'returns environment with enable_advanced_logs_querying and logs_api_path' do - get api("/projects/#{project.id}/environments", 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(1) - expect(json_response.first['enable_advanced_logs_querying']).to eq(true) - expect(json_response.first['logs_api_path']).to eq( - "/#{project.full_path}/-/logs/elasticsearch.json?environment_name=#{environment.name}" - ) - end - end - - context 'when elastic stack is not available' do - before do - allow_next_found_instance_of(Environment) do |env| - allow(env).to receive(:elastic_stack_available?).and_return(false) - end - end - - it 'returns environment with enable_advanced_logs_querying logs_api_path' do - get api("/projects/#{project.id}/environments", 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(1) - expect(json_response.first['enable_advanced_logs_querying']).to eq(false) - expect(json_response.first['logs_api_path']).to eq( - "/#{project.full_path}/-/logs/k8s.json?environment_name=#{environment.name}" - ) - end - end - end - - context 'when the user cannot read pod logs' do - before do - allow_next_found_instance_of(User) do |user| - allow(user).to receive(:can?).and_call_original - allow(user).to receive(:can?).with(:read_pod_logs, project).and_return(false) - end - end - - it 'does not contain enable_advanced_logs_querying' do - get api("/projects/#{project.id}/environments", 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(1) - expect(json_response.first).not_to have_key('enable_advanced_logs_querying') - expect(json_response.first).not_to have_key('logs_api_path') - expect(json_response.first).not_to have_key('gitlab_managed_apps_logs_path') - end end context 'when filtering' do diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb index 110d6e2f99e..d6c3999f22f 100644 --- a/spec/requests/api/events_spec.rb +++ b/spec/requests/api/events_spec.rb @@ -173,7 +173,7 @@ RSpec.describe API::Events do let(:second_note) { create(:note_on_issue, project: create(:project)) } before do - second_note.project.add_user(user, :developer) + second_note.project.add_member(user, :developer) [second_note].each do |note| EventCreateService.new.leave_note(note, user) diff --git a/spec/requests/api/feature_flags_user_lists_spec.rb b/spec/requests/api/feature_flags_user_lists_spec.rb index e2a3f92df10..bfc57042ff4 100644 --- a/spec/requests/api/feature_flags_user_lists_spec.rb +++ b/spec/requests/api/feature_flags_user_lists_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe API::FeatureFlagsUserLists do let_it_be(:project, refind: true) { create(:project) } + let_it_be(:client, refind: true) { create(:operations_feature_flags_client, project: project) } let_it_be(:developer) { create(:user) } let_it_be(:reporter) { create(:user) } @@ -215,6 +216,7 @@ RSpec.describe API::FeatureFlagsUserLists do } expect(response).to have_gitlab_http_status(:forbidden) + expect(client.reload.last_feature_flag_updated_at).to be_nil end it 'creates the flag' do @@ -231,6 +233,7 @@ RSpec.describe API::FeatureFlagsUserLists do }) expect(project.operations_feature_flags_user_lists.count).to eq(1) expect(project.operations_feature_flags_user_lists.last.name).to eq('mylist') + expect(client.reload.last_feature_flag_updated_at).not_to be_nil end it 'requires name' do @@ -298,6 +301,7 @@ RSpec.describe API::FeatureFlagsUserLists do } expect(response).to have_gitlab_http_status(:forbidden) + expect(client.reload.last_feature_flag_updated_at).to be_nil end it 'updates the list' do @@ -313,6 +317,7 @@ RSpec.describe API::FeatureFlagsUserLists do 'user_xids' => '456,789' }) expect(list.reload.name).to eq('mylist') + expect(client.reload.last_feature_flag_updated_at).not_to be_nil end it 'preserves attributes not listed in the request' do @@ -377,6 +382,7 @@ RSpec.describe API::FeatureFlagsUserLists do expect(response).to have_gitlab_http_status(:not_found) expect(json_response).to eq({ 'message' => '404 Not found' }) + expect(client.reload.last_feature_flag_updated_at).to be_nil end it 'deletes the list' do @@ -387,6 +393,7 @@ RSpec.describe API::FeatureFlagsUserLists do expect(response).to have_gitlab_http_status(:no_content) expect(response.body).to be_blank expect(project.operations_feature_flags_user_lists.count).to eq(0) + expect(client.reload.last_feature_flag_updated_at).not_to be_nil end it 'does not delete the list if it is associated with a strategy' do diff --git a/spec/requests/api/geo_spec.rb b/spec/requests/api/geo_spec.rb index edbca5eb1c6..4e77fa9405c 100644 --- a/spec/requests/api/geo_spec.rb +++ b/spec/requests/api/geo_spec.rb @@ -10,12 +10,24 @@ RSpec.describe API::Geo do include_context 'workhorse headers' + let(:non_proxy_response_schema) do + { + 'type' => 'object', + 'additionalProperties' => false, + 'required' => %w(geo_enabled), + 'properties' => { + 'geo_enabled' => { 'type' => 'boolean' } + } + } + end + context 'with valid auth' do it 'returns empty data' do subject expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_empty + expect(json_response).to match_schema(non_proxy_response_schema) + expect(json_response['geo_enabled']).to be_falsey end end diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb index eb206465bce..39ff108a9e1 100644 --- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb @@ -96,7 +96,8 @@ RSpec.describe 'get board lists' do context 'when ascending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { } + include_context 'no sort argument' + let(:first_param) { 2 } let(:all_records) { lists.map { |list| a_graphql_entity_for(list) } } end diff --git a/spec/requests/api/graphql/ci/group_variables_spec.rb b/spec/requests/api/graphql/ci/group_variables_spec.rb new file mode 100644 index 00000000000..5ea6646ec2c --- /dev/null +++ b/spec/requests/api/graphql/ci/group_variables_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.group(fullPath).ciVariables' do + include GraphqlHelpers + + let_it_be(:group) { create(:group) } + let_it_be(:user) { create(:user) } + + let(:query) do + %( + query { + group(fullPath: "#{group.full_path}") { + ciVariables { + nodes { + id + key + value + variableType + protected + masked + raw + environmentScope + } + } + } + } + ) + end + + context 'when the user can administer the group' do + before do + group.add_owner(user) + end + + it "returns the group's CI variables" do + variable = create(:ci_group_variable, group: group, key: 'TEST_VAR', value: 'test', + masked: false, protected: true, raw: true, environment_scope: 'staging') + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('group', 'ciVariables', 'nodes')).to contain_exactly({ + 'id' => variable.to_global_id.to_s, + 'key' => 'TEST_VAR', + 'value' => 'test', + 'variableType' => 'ENV_VAR', + 'masked' => false, + 'protected' => true, + 'raw' => true, + 'environmentScope' => 'staging' + }) + end + end + + context 'when the user cannot administer the group' do + it 'returns nothing' do + create(:ci_group_variable, group: group, value: 'verysecret', masked: true) + + group.add_developer(user) + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('group', 'ciVariables')).to be_nil + end + end +end diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb new file mode 100644 index 00000000000..7acf73a4e7a --- /dev/null +++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.ciVariables' do + include GraphqlHelpers + + let(:query) do + %( + query { + ciVariables { + nodes { + id + key + value + variableType + protected + masked + raw + environmentScope + } + } + } + ) + end + + context 'when the user is an admin' do + let_it_be(:user) { create(:admin) } + + it "returns the instance's CI variables" do + variable = create(:ci_instance_variable, key: 'TEST_VAR', value: 'test', + masked: false, protected: true, raw: true) + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('ciVariables', 'nodes')).to contain_exactly({ + 'id' => variable.to_global_id.to_s, + 'key' => 'TEST_VAR', + 'value' => 'test', + 'variableType' => 'ENV_VAR', + 'masked' => false, + 'protected' => true, + 'raw' => true, + 'environmentScope' => nil + }) + end + end + + context 'when the user is not an admin' do + let_it_be(:user) { create(:user) } + + it 'returns nothing' do + create(:ci_instance_variable, value: 'verysecret', masked: true) + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('ciVariables')).to be_nil + end + end +end diff --git a/spec/requests/api/graphql/ci/job_spec.rb b/spec/requests/api/graphql/ci/job_spec.rb index 2fb90dcd92b..3721155c71b 100644 --- a/spec/requests/api/graphql/ci/job_spec.rb +++ b/spec/requests/api/graphql/ci/job_spec.rb @@ -13,8 +13,8 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do let_it_be(:project) { create(:project, :repository, :public) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) } - let_it_be(:prepare_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'prepare') } - let_it_be(:test_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'test') } + let_it_be(:prepare_stage) { create(:ci_stage, pipeline: pipeline, project: project, name: 'prepare') } + let_it_be(:test_stage) { create(:ci_stage, pipeline: pipeline, project: project, name: 'test') } let_it_be(:job_1) { create(:ci_build, pipeline: pipeline, stage: 'prepare', name: 'Job 1') } let_it_be(:job_2) { create(:ci_build, pipeline: pipeline, stage: 'test', name: 'Job 2') } diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb index d1737fc22ae..8c4ab13fc35 100644 --- a/spec/requests/api/graphql/ci/jobs_spec.rb +++ b/spec/requests/api/graphql/ci/jobs_spec.rb @@ -14,7 +14,7 @@ RSpec.describe 'Query.project.pipeline' do describe '.stages.groups.jobs' do let(:pipeline) do pipeline = create(:ci_pipeline, project: project, user: user) - stage = create(:ci_stage_entity, project: project, pipeline: pipeline, name: 'first', position: 1) + stage = create(:ci_stage, project: project, pipeline: pipeline, name: 'first', position: 1) create(:ci_build, stage_id: stage.id, pipeline: pipeline, name: 'my test job', scheduling_type: :stage) pipeline @@ -84,8 +84,8 @@ RSpec.describe 'Query.project.pipeline' do context 'when there is more than one stage and job needs' do before do - build_stage = create(:ci_stage_entity, position: 2, name: 'build', project: project, pipeline: pipeline) - test_stage = create(:ci_stage_entity, position: 3, name: 'test', project: project, pipeline: pipeline) + build_stage = create(:ci_stage, position: 2, name: 'build', project: project, pipeline: pipeline) + test_stage = create(:ci_stage, position: 3, name: 'test', project: project, pipeline: pipeline) create(:ci_build, pipeline: pipeline, name: 'docker 1 2', scheduling_type: :stage, stage: build_stage, stage_idx: build_stage.position) create(:ci_build, pipeline: pipeline, name: 'docker 2 2', stage: build_stage, stage_idx: build_stage.position, scheduling_type: :dag) diff --git a/spec/requests/api/graphql/ci/manual_variables_spec.rb b/spec/requests/api/graphql/ci/manual_variables_spec.rb new file mode 100644 index 00000000000..b7aa76511a3 --- /dev/null +++ b/spec/requests/api/graphql/ci/manual_variables_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:user) { create(:user) } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + pipelines { + nodes { + jobs { + nodes { + manualVariables { + nodes { + key + } + } + } + } + } + } + } + } + ) + end + + before do + project.add_maintainer(user) + end + + it 'returns the manual variables for the jobs' do + job = create(:ci_build, :manual, pipeline: pipeline) + create(:ci_job_variable, key: 'MANUAL_TEST_VAR', job: job) + + post_graphql(query, current_user: user) + + variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first + .dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') } + expect(variables_data.map { |var| var['key'] }).to match_array(['MANUAL_TEST_VAR']) + end + + it 'does not fetch job variables for jobs that are not manual' do + job = create(:ci_build, pipeline: pipeline) + create(:ci_job_variable, key: 'THIS_VAR_WOULD_SHOULD_NEVER_EXIST', job: job) + + post_graphql(query, current_user: user) + + variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first + .dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') } + expect(variables_data).to be_empty + end + + it 'does not fetch job variables for bridges' do + create(:ci_bridge, :manual, pipeline: pipeline) + + post_graphql(query, current_user: user) + + variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first + .dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') } + expect(variables_data).to be_empty + end + + it 'does not produce N+1 queries', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/367991' do + second_user = create(:user) + project.add_maintainer(second_user) + job = create(:ci_build, :manual, pipeline: pipeline) + create(:ci_job_variable, key: 'MANUAL_TEST_VAR_1', job: job) + + control_count = ActiveRecord::QueryRecorder.new do + post_graphql(query, current_user: user) + end + + variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first + .dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') } + expect(variables_data.map { |var| var['key'] }).to match_array(['MANUAL_TEST_VAR_1']) + + job = create(:ci_build, :manual, pipeline: pipeline) + create(:ci_job_variable, key: 'MANUAL_TEST_VAR_2', job: job) + + expect do + post_graphql(query, current_user: second_user) + end.not_to exceed_query_limit(control_count) + + variables_data = graphql_data.dig('project', 'pipelines', 'nodes').first + .dig('jobs', 'nodes').flat_map { |job| job.dig('manualVariables', 'nodes') } + expect(variables_data.map { |var| var['key'] }).to match_array(%w(MANUAL_TEST_VAR_1 MANUAL_TEST_VAR_2)) + end +end diff --git a/spec/requests/api/graphql/ci/pipelines_spec.rb b/spec/requests/api/graphql/ci/pipelines_spec.rb index 741af676b6d..a968e5508cb 100644 --- a/spec/requests/api/graphql/ci/pipelines_spec.rb +++ b/spec/requests/api/graphql/ci/pipelines_spec.rb @@ -86,8 +86,8 @@ RSpec.describe 'Query.project(fullPath).pipelines' do describe '.stages' do let_it_be(:project) { create(:project, :repository) } let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project) } - let_it_be(:stage) { create(:ci_stage_entity, pipeline: pipeline, project: project) } - let_it_be(:other_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'other') } + let_it_be(:stage) { create(:ci_stage, pipeline: pipeline, project: project) } + let_it_be(:other_stage) { create(:ci_stage, pipeline: pipeline, project: project, name: 'other') } let(:first_n) { var('Int') } let(:query_path) do diff --git a/spec/requests/api/graphql/ci/project_variables_spec.rb b/spec/requests/api/graphql/ci/project_variables_spec.rb new file mode 100644 index 00000000000..e61f146b24c --- /dev/null +++ b/spec/requests/api/graphql/ci/project_variables_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Query.project(fullPath).ciVariables' do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + ciVariables { + nodes { + id + key + value + variableType + protected + masked + raw + environmentScope + } + } + } + } + ) + end + + context 'when the user can administer builds' do + before do + project.add_maintainer(user) + end + + it "returns the project's CI variables" do + variable = create(:ci_variable, project: project, key: 'TEST_VAR', value: 'test', + masked: false, protected: true, raw: true, environment_scope: 'production') + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'ciVariables', 'nodes')).to contain_exactly({ + 'id' => variable.to_global_id.to_s, + 'key' => 'TEST_VAR', + 'value' => 'test', + 'variableType' => 'ENV_VAR', + 'masked' => false, + 'protected' => true, + 'raw' => true, + 'environmentScope' => 'production' + }) + end + end + + context 'when the user cannot administer builds' do + it 'returns nothing' do + create(:ci_variable, project: project, value: 'verysecret', masked: true) + + project.add_developer(user) + + post_graphql(query, current_user: user) + + expect(graphql_data.dig('project', 'ciVariables')).to be_nil + end + end +end diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb index 446d1fb1bdb..e17a83d8e47 100644 --- a/spec/requests/api/graphql/ci/runner_spec.rb +++ b/spec/requests/api/graphql/ci/runner_spec.rb @@ -424,7 +424,7 @@ RSpec.describe 'Query.runner(id)' do let(:user) { create(:user) } before do - group.add_user(user, Gitlab::Access::OWNER) + group.add_member(user, Gitlab::Access::OWNER) end it_behaves_like 'retrieval with no admin url' do diff --git a/spec/requests/api/graphql/ci/stages_spec.rb b/spec/requests/api/graphql/ci/stages_spec.rb index 50d2cf75097..1edd6e58486 100644 --- a/spec/requests/api/graphql/ci/stages_spec.rb +++ b/spec/requests/api/graphql/ci/stages_spec.rb @@ -36,7 +36,7 @@ RSpec.describe 'Query.project.pipeline.stages' do end before_all do - create(:ci_stage_entity, pipeline: pipeline, name: 'deploy') + create(:ci_stage, pipeline: pipeline, name: 'deploy') create_list(:ci_build, 2, pipeline: pipeline, stage: 'deploy') end diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb index 847fa72522e..14c55e61a65 100644 --- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb +++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb @@ -71,7 +71,7 @@ RSpec.describe 'container repository details' do 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 + project.add_member(user, role) unless role == :anonymous end it 'return the proper response' do diff --git a/spec/requests/api/graphql/crm/contacts_spec.rb b/spec/requests/api/graphql/crm/contacts_spec.rb new file mode 100644 index 00000000000..7e824140894 --- /dev/null +++ b/spec/requests/api/graphql/crm/contacts_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting CRM contacts' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group, :crm_enabled) } + + let_it_be(:contact_a) do + create( + :contact, + group: group, + first_name: "ABC", + last_name: "DEF", + email: "ghi@test.com", + description: "LMNO", + state: "inactive" + ) + end + + let_it_be(:contact_b) do + create( + :contact, + group: group, + first_name: "ABC", + last_name: "DEF", + email: "vwx@test.com", + description: "YZ", + state: "active" + ) + end + + let_it_be(:contact_c) do + create( + :contact, + group: group, + first_name: "PQR", + last_name: "STU", + email: "aaa@test.com", + description: "YZ", + state: "active" + ) + end + + before do + group.add_reporter(current_user) + end + + it_behaves_like 'sorted paginated query' do + let(:sort_argument) { {} } + let(:first_param) { 2 } + let(:all_records) { [contact_a, contact_b, contact_c] } + let(:data_path) { [:group, :contacts] } + + def pagination_query(params) + graphql_query_for( + :group, + { full_path: group.full_path }, + query_graphql_field(:contacts, params, "#{page_info} nodes { id }") + ) + end + + def pagination_results_data(nodes) + nodes.map { |item| GlobalID::Locator.locate(item['id']) } + end + end +end diff --git a/spec/requests/api/graphql/current_user/groups_query_spec.rb b/spec/requests/api/graphql/current_user/groups_query_spec.rb index 39f323b21a3..ef0f32bacf0 100644 --- a/spec/requests/api/graphql/current_user/groups_query_spec.rb +++ b/spec/requests/api/graphql/current_user/groups_query_spec.rb @@ -8,8 +8,9 @@ RSpec.describe 'Query current user groups' do let_it_be(:user) { create(:user) } let_it_be(:guest_group) { create(:group, name: 'public guest', path: 'public-guest') } let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer') } - let_it_be(:public_developer_group) { create(:group, :private, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') } - let_it_be(:public_maintainer_group) { create(:group, :private, name: 'a public maintainer', path: 'a-public-maintainer') } + let_it_be(:public_developer_group) { create(:group, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') } + let_it_be(:public_maintainer_group) { create(:group, name: 'a public maintainer', path: 'a-public-maintainer') } + let_it_be(:public_owner_group) { create(:group, name: 'a public owner', path: 'a-public-owner') } let(:group_arguments) { {} } let(:current_user) { user } @@ -29,6 +30,7 @@ RSpec.describe 'Query current user groups' do private_maintainer_group.add_maintainer(user) public_developer_group.add_developer(user) public_maintainer_group.add_maintainer(user) + public_owner_group.add_owner(user) end subject { graphql_data.dig('currentUser', 'groups', 'nodes') } @@ -52,6 +54,7 @@ RSpec.describe 'Query current user groups' do is_expected.to match( expected_group_hash( public_maintainer_group, + public_owner_group, private_maintainer_group, public_developer_group, guest_group @@ -66,6 +69,7 @@ RSpec.describe 'Query current user groups' do is_expected.to match( expected_group_hash( public_maintainer_group, + public_owner_group, private_maintainer_group, public_developer_group ) @@ -86,6 +90,32 @@ RSpec.describe 'Query current user groups' do end end + context 'when permission_scope is TRANSFER_PROJECTS' do + let(:group_arguments) { { permission_scope: :TRANSFER_PROJECTS } } + + specify do + is_expected.to match( + expected_group_hash( + public_maintainer_group, + public_owner_group, + private_maintainer_group + ) + ) + end + + context 'when search is provided' do + let(:group_arguments) { { permission_scope: :TRANSFER_PROJECTS, search: 'owner' } } + + specify do + is_expected.to match( + expected_group_hash( + public_owner_group + ) + ) + end + end + end + context 'when search is provided' do let(:group_arguments) { { search: 'maintainer' } } diff --git a/spec/requests/api/graphql/group/container_repositories_spec.rb b/spec/requests/api/graphql/group/container_repositories_spec.rb index be0b866af4a..8ec321c8d7c 100644 --- a/spec/requests/api/graphql/group/container_repositories_spec.rb +++ b/spec/requests/api/graphql/group/container_repositories_spec.rb @@ -82,7 +82,7 @@ RSpec.describe 'getting container repositories in a group' 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 + group.add_member(user, role) unless role == :anonymous end it 'return the proper response' do diff --git a/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb index cdb21512894..daa1483e956 100644 --- a/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb +++ b/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb @@ -75,7 +75,7 @@ RSpec.describe 'getting dependency proxy blobs in a group' do with_them do before do group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false)) - group.add_user(user, role) unless role == :anonymous + group.add_member(user, role) unless role == :anonymous end it 'return the proper response' do diff --git a/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb index d21c3046c1a..cc706c3051f 100644 --- a/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb +++ b/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb @@ -61,7 +61,7 @@ RSpec.describe 'getting dependency proxy settings for a group' do with_them do before do group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false)) - group.add_user(user, role) unless role == :anonymous + group.add_member(user, role) unless role == :anonymous end it 'return the proper response' do diff --git a/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb index 40f4b082072..3b2b04b1322 100644 --- a/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb +++ b/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb @@ -60,7 +60,7 @@ RSpec.describe 'getting dependency proxy image ttl policy for a group' do with_them do before do group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false)) - group.add_user(user, role) unless role == :anonymous + group.add_member(user, role) unless role == :anonymous end it 'return the proper response' do diff --git a/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb index c7149c100b2..37ef7089c2f 100644 --- a/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb +++ b/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb @@ -73,7 +73,7 @@ RSpec.describe 'getting dependency proxy manifests in a group' do with_them do before do group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility.to_s.upcase, false)) - group.add_user(user, role) unless role == :anonymous + group.add_member(user, role) unless role == :anonymous end it 'return the proper response' do @@ -125,7 +125,8 @@ RSpec.describe 'getting dependency proxy manifests in a group' do let_it_be(:descending_manifests) { manifests.reverse.map { |manifest| global_id_of(manifest) } } it_behaves_like 'sorted paginated query' do - let(:sort_param) { '' } + include_context 'no sort argument' + let(:first_param) { 2 } let(:all_records) { descending_manifests.map(&:to_s) } end @@ -134,7 +135,7 @@ RSpec.describe 'getting dependency proxy manifests in a group' do def pagination_query(params) # remove sort since the type does not accept sorting, but be future proof graphql_query_for('group', { 'fullPath' => group.full_path }, - query_nodes(:dependencyProxyManifests, :id, include_pagination_info: true, args: params.merge(sort: nil)) + query_nodes(:dependencyProxyManifests, :id, include_pagination_info: true, args: params) ) end end diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb index fec866486ae..1ff5b134e92 100644 --- a/spec/requests/api/graphql/group/group_members_spec.rb +++ b/spec/requests/api/graphql/group/group_members_spec.rb @@ -7,8 +7,8 @@ RSpec.describe 'getting group members information' do let_it_be(:parent_group) { create(:group, :public) } let_it_be(:user) { create(:user) } - let_it_be(:user_1) { create(:user, username: 'user') } - let_it_be(:user_2) { create(:user, username: 'test') } + let_it_be(:user_1) { create(:user, username: 'user', name: 'Same Name') } + let_it_be(:user_2) { create(:user, username: 'test', name: 'Same Name') } before_all do [user_1, user_2].each { |user| parent_group.add_guest(user) } @@ -45,11 +45,44 @@ RSpec.describe 'getting group members information' do expect_array_response(user_1, user_2) end - it 'returns members that match the search query' do - fetch_members(args: { search: 'test' }) + describe 'search argument' do + it 'returns members that match the search query' do + fetch_members(args: { search: 'test' }) - expect(graphql_errors).to be_nil - expect_array_response(user_2) + expect(graphql_errors).to be_nil + expect_array_response(user_2) + end + + context 'when paginating' do + it 'returns correct results' do + fetch_members(args: { search: 'Same Name', first: 1 }) + + expect_array_response(user_1) + + next_cursor = graphql_data_at(:group, :groupMembers, :pageInfo, :endCursor) + fetch_members(args: { search: 'Same Name', first: 1, after: next_cursor }) + + expect_array_response(user_2) + end + + context 'when the use_keyset_aware_user_search_query FF is off' do + before do + stub_feature_flags(use_keyset_aware_user_search_query: false) + end + + it 'raises error on the 2nd page due to missing cursor data' do + fetch_members(args: { search: 'Same Name', first: 1 }) + + # user_2 because the "old" order was undeterministic (insert order), no tie-breaker column + expect_array_response(user_2) + + next_cursor = graphql_data_at(:group, :groupMembers, :pageInfo, :endCursor) + fetch_members(args: { search: 'Same Name', first: 1, after: next_cursor }) + + expect(graphql_errors.first['message']).to include('PG::UndefinedColumn') + end + end + end end end @@ -196,6 +229,9 @@ RSpec.describe 'getting group members information' do } } } + pageInfo { + endCursor + } NODE graphql_query_for("group", diff --git a/spec/requests/api/graphql/mutations/issues/create_spec.rb b/spec/requests/api/graphql/mutations/issues/create_spec.rb index 3d81b456c9c..9345735afe4 100644 --- a/spec/requests/api/graphql/mutations/issues/create_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/create_spec.rb @@ -53,6 +53,42 @@ RSpec.describe 'Create an issue' do let(:mutation_class) { ::Mutations::Issues::Create } end + context 'when creating an issue of type TASK' do + before do + input['type'] = 'TASK' + end + + context 'when work_items feature flag is disabled' do + before do + stub_feature_flags(work_items: false) + end + + it 'creates an issue with the default ISSUE type' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change(Issue, :count).by(1) + + created_issue = Issue.last + + expect(created_issue.work_item_type.base_type).to eq('issue') + expect(created_issue.issue_type).to eq('issue') + end + end + + context 'when work_items feature flag is enabled' do + it 'creates an issue with TASK type' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change(Issue, :count).by(1) + + created_issue = Issue.last + + expect(created_issue.work_item_type.base_type).to eq('task') + expect(created_issue.issue_type).to eq('task') + end + end + end + context 'when position params are provided' do let(:existing_issue) { create(:issue, project: project, relative_position: 50) } diff --git a/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb index 8f3ae9f26f6..a432fb17a70 100644 --- a/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/create/diff_note_spec.rb @@ -10,11 +10,12 @@ RSpec.describe 'Adding a DiffNote' do let(:noteable) { create(:merge_request, source_project: project, target_project: project) } let(:project) { create(:project, :repository) } let(:diff_refs) { noteable.diff_refs } + let(:body) { 'Body text' } let(:base_variables) do { noteable_id: GitlabSchema.id_from_object(noteable).to_s, - body: 'Body text', + body: body, position: { paths: { old_path: 'files/ruby/popen.rb', @@ -65,6 +66,17 @@ RSpec.describe 'Adding a DiffNote' do it_behaves_like 'a Note mutation when the given resource id is not for a Noteable' end + context 'with /merge quick action' do + let(:body) { "Body text \n/merge" } + + it 'merges the merge request', :sidekiq_inline do + post_graphql_mutation(mutation, current_user: current_user) + + expect(noteable.reload.state).to eq('merged') + expect(mutation_response['note']['body']).to eq('Body text') + end + end + it 'returns the note with the correct position' do post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb index eb7e6f840fe..1a5d3620f22 100644 --- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe 'Updating a Snippet' do include GraphqlHelpers + include SessionHelpers let_it_be(:original_content) { 'Initial content' } let_it_be(:original_description) { 'Initial description' } @@ -162,7 +163,7 @@ RSpec.describe 'Updating a Snippet' do end end - context 'when the author is a member of the project' do + context 'when the author is a member of the project', :snowplow do before do project.add_developer(current_user) end @@ -185,6 +186,20 @@ RSpec.describe 'Updating a Snippet' do it_behaves_like 'has spam protection' do let(:mutation_class) { ::Mutations::Snippets::Update } end + + context 'when not sessionless', :clean_gitlab_redis_sessions do + before do + stub_session('warden.user.user.key' => [[current_user.id], current_user.authenticatable_salt]) + end + + it_behaves_like 'Snowplow event tracking' do + let(:user) { current_user } + let(:namespace) { project.namespace } + let(:category) { 'ide_edit' } + let(:action) { 'g_edit_by_snippet_ide' } + let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } + end + end end it_behaves_like 'when the snippet is not found' diff --git a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb index 8d33f8e1806..b1356bbe6fd 100644 --- a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb @@ -47,6 +47,7 @@ RSpec.describe "Create a work item from a task in a work item's description" do expect(work_item.description).to eq("- [ ] #{created_work_item.to_reference}+") expect(created_work_item.issue_type).to eq('task') expect(created_work_item.work_item_type.base_type).to eq('task') + expect(created_work_item.work_item_parent).to eq(work_item) expect(mutation_response['workItem']).to include('id' => work_item.to_global_id.to_s) expect(mutation_response['newWorkItem']).to include('id' => created_work_item.to_global_id.to_s) end diff --git a/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_spec.rb index 6abdaa2c850..911568bc39f 100644 --- a/spec/requests/api/graphql/mutations/work_items/create_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb @@ -63,6 +63,95 @@ RSpec.describe 'Create a work item' do let(:mutation_class) { ::Mutations::WorkItems::Create } end + context 'with hierarchy widget input' do + let(:widgets_response) { mutation_response['workItem']['widgets'] } + let(:fields) do + <<~FIELDS + workItem { + widgets { + type + ... on WorkItemWidgetHierarchy { + parent { + id + } + children { + edges { + node { + id + } + } + } + } + } + } + errors + FIELDS + end + + let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) } + + context 'when setting parent' do + let_it_be(:parent) { create(:work_item, project: project) } + + let(:input) do + { + title: 'item1', + workItemTypeId: WorkItems::Type.default_by_type(:task).to_global_id.to_s, + hierarchyWidget: { 'parentId' => parent.to_global_id.to_s } + } + end + + it 'updates the work item parent' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(widgets_response).to include( + { + 'children' => { 'edges' => [] }, + 'parent' => { 'id' => parent.to_global_id.to_s }, + 'type' => 'HIERARCHY' + } + ) + end + + context 'when parent work item type is invalid' do + let_it_be(:parent) { create(:work_item, :task, project: project) } + + it 'returns error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['errors']) + .to contain_exactly(/cannot be added: only Issue and Incident can be parent of Task./) + expect(mutation_response['workItem']).to be_nil + end + end + + context 'when parent work item is not found' do + let_it_be(:parent) { build_stubbed(:work_item, id: non_existing_record_id)} + + it 'returns a top level error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_errors.first['message']).to include('No object found for `parentId') + end + end + end + + context 'when unsupported widget input is sent' do + let(:input) do + { + 'title' => 'new title', + 'description' => 'new description', + 'workItemTypeId' => WorkItems::Type.default_by_type(:test_case).to_global_id.to_s, + 'hierarchyWidget' => {} + } + end + + it_behaves_like 'a mutation that returns top-level errors', + errors: ['Following widget keys are not supported by Test Case type: [:hierarchy_widget]'] + end + end + context 'when the work_items feature flag is disabled' do before do stub_feature_flags(work_items: false) diff --git a/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb index 05d3587d342..e576d0ee7ef 100644 --- a/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/delete_task_spec.rb @@ -54,7 +54,7 @@ RSpec.describe "Delete a task in a work item's description" do end.to change(WorkItem, :count).by(-1).and( change(IssueLink, :count).by(-1) ).and( - change(work_item, :description).from("- [ ] #{task.to_reference}+").to('') + change(work_item, :description).from("- [ ] #{task.to_reference}+").to("- [ ] #{task.title}") ) expect(response).to have_gitlab_http_status(:success) diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb index 71b03103115..77f7b9bacef 100644 --- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb @@ -11,8 +11,17 @@ RSpec.describe 'Update a work item' do let(:work_item_event) { 'CLOSE' } let(:input) { { 'stateEvent' => work_item_event, 'title' => 'updated title' } } + let(:fields) do + <<~FIELDS + workItem { + state + title + } + errors + FIELDS + end - let(:mutation) { graphql_mutation(:workItemUpdate, input.merge('id' => work_item.to_global_id.to_s)) } + let(:mutation) { graphql_mutation(:workItemUpdate, input.merge('id' => work_item.to_global_id.to_s), fields) } let(:mutation_response) { graphql_mutation_response(:work_item_update) } @@ -62,6 +71,20 @@ RSpec.describe 'Update a work item' do end end + context 'when unsupported widget input is sent' do + let_it_be(:test_case) { create(:work_item_type, :default, :test_case, name: 'some_test_case_name') } + let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) } + + let(:input) do + { + 'hierarchyWidget' => {} + } + end + + it_behaves_like 'a mutation that returns top-level errors', + errors: ["Following widget keys are not supported by some_test_case_name type: [:hierarchy_widget]"] + end + it_behaves_like 'has spam protection' do let(:mutation_class) { ::Mutations::WorkItems::Update } end @@ -80,5 +103,248 @@ RSpec.describe 'Update a work item' do expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project') end end + + context 'with description widget input' do + let(:fields) do + <<~FIELDS + workItem { + description + widgets { + type + ... on WorkItemWidgetDescription { + description + } + } + } + errors + FIELDS + end + + it_behaves_like 'update work item description widget' do + let(:new_description) { 'updated description' } + let(:input) do + { 'descriptionWidget' => { 'description' => new_description } } + end + end + end + + context 'with weight widget input' do + let(:fields) do + <<~FIELDS + workItem { + widgets { + type + ... on WorkItemWidgetWeight { + weight + } + } + } + errors + FIELDS + end + + it_behaves_like 'update work item weight widget' do + let(:new_weight) { 2 } + + let(:input) do + { 'weightWidget' => { 'weight' => new_weight } } + end + end + end + + context 'with hierarchy widget input' do + let(:widgets_response) { mutation_response['workItem']['widgets'] } + let(:fields) do + <<~FIELDS + workItem { + description + widgets { + type + ... on WorkItemWidgetHierarchy { + parent { + id + } + children { + edges { + node { + id + } + } + } + } + } + } + errors + FIELDS + end + + context 'when updating parent' do + let_it_be(:work_item) { create(:work_item, :task, project: project) } + let_it_be(:valid_parent) { create(:work_item, project: project) } + let_it_be(:invalid_parent) { create(:work_item, :task, project: project) } + + context 'when parent work item type is invalid' do + let(:error) { "#{work_item.to_reference} cannot be added: only Issue and Incident can be parent of Task." } + let(:input) do + { 'hierarchyWidget' => { 'parentId' => invalid_parent.to_global_id.to_s }, 'title' => 'new title' } + end + + it 'returns response with errors' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to not_change(work_item, :work_item_parent).and(not_change(work_item, :title)) + + expect(mutation_response['workItem']).to be_nil + expect(mutation_response['errors']).to match_array([error]) + end + end + + context 'when parent work item has a valid type' do + let(:input) { { 'hierarchyWidget' => { 'parentId' => valid_parent.to_global_id.to_s } } } + + it 'sets the parent for the work item' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :work_item_parent).from(nil).to(valid_parent) + + expect(response).to have_gitlab_http_status(:success) + expect(widgets_response).to include( + { + 'children' => { 'edges' => [] }, + 'parent' => { 'id' => valid_parent.to_global_id.to_s }, + 'type' => 'HIERARCHY' + } + ) + end + + context 'when a parent is already present' do + let_it_be(:existing_parent) { create(:work_item, project: project) } + + before do + work_item.update!(work_item_parent: existing_parent) + end + + it 'is replaced with new parent' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :work_item_parent).from(existing_parent).to(valid_parent) + end + end + end + + context 'when parentId is null' do + let(:input) { { 'hierarchyWidget' => { 'parentId' => nil } } } + + context 'when parent is present' do + before do + work_item.update!(work_item_parent: valid_parent) + end + + it 'removes parent and returns success message' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :work_item_parent).from(valid_parent).to(nil) + + expect(response).to have_gitlab_http_status(:success) + expect(widgets_response) + .to include( + { + 'children' => { 'edges' => [] }, + 'parent' => nil, + 'type' => 'HIERARCHY' + } + ) + end + end + + context 'when parent is not present' do + before do + work_item.update!(work_item_parent: nil) + end + + it 'does not change work item and returns success message' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.not_to change(work_item, :work_item_parent) + + expect(response).to have_gitlab_http_status(:success) + end + end + end + + context 'when parent work item is not found' do + let(:input) { { 'hierarchyWidget' => { 'parentId' => "gid://gitlab/WorkItem/#{non_existing_record_id}" } } } + + it 'returns a top level error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(graphql_errors.first['message']).to include('No object found for `parentId') + end + end + end + + context 'when updating children' do + let_it_be(:valid_child1) { create(:work_item, :task, project: project) } + let_it_be(:valid_child2) { create(:work_item, :task, project: project) } + let_it_be(:invalid_child) { create(:work_item, project: project) } + + let(:input) { { 'hierarchyWidget' => { 'childrenIds' => children_ids } } } + let(:error) do + "#{invalid_child.to_reference} cannot be added: only Task can be assigned as a child in hierarchy." + end + + context 'when child work item type is invalid' do + let(:children_ids) { [invalid_child.to_global_id.to_s] } + + it 'returns response with errors' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['workItem']).to be_nil + expect(mutation_response['errors']).to match_array([error]) + end + end + + context 'when there is a mix of existing and non existing work items' do + let(:children_ids) { [valid_child1.to_global_id.to_s, "gid://gitlab/WorkItem/#{non_existing_record_id}"] } + + it 'returns a top level error and does not add valid work item' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.not_to change(work_item.work_item_children, :count) + + expect(graphql_errors.first['message']).to include('No object found for `childrenIds') + end + end + + context 'when child work item type is valid' do + let(:children_ids) { [valid_child1.to_global_id.to_s, valid_child2.to_global_id.to_s] } + + it 'updates the work item children' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item.work_item_children, :count).by(2) + + expect(response).to have_gitlab_http_status(:success) + expect(widgets_response).to include( + { + 'children' => { 'edges' => [ + { 'node' => { 'id' => valid_child2.to_global_id.to_s } }, + { 'node' => { 'id' => valid_child1.to_global_id.to_s } } + ] }, + 'parent' => nil, + 'type' => 'HIERARCHY' + } + ) + end + end + end + end end end diff --git a/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb index 595d8fe97ed..2a5cb937a2f 100644 --- a/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/update_widgets_spec.rb @@ -9,16 +9,23 @@ RSpec.describe 'Update work item widgets' do let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } } let_it_be(:work_item, refind: true) { create(:work_item, project: project) } - let(:input) do - { - 'descriptionWidget' => { 'description' => 'updated description' } + let(:input) { { 'descriptionWidget' => { 'description' => 'updated description' } } } + let(:mutation_response) { graphql_mutation_response(:work_item_update_widgets) } + let(:mutation) do + graphql_mutation(:workItemUpdateWidgets, input.merge('id' => work_item.to_global_id.to_s), <<~FIELDS) + errors + workItem { + description + widgets { + type + ... on WorkItemWidgetDescription { + description + } + } } + FIELDS end - let(:mutation) { graphql_mutation(:workItemUpdateWidgets, input.merge('id' => work_item.to_global_id.to_s)) } - - let(:mutation_response) { graphql_mutation_response(:work_item_update_widgets) } - context 'the user is not allowed to update a work item' do let(:current_user) { create(:user) } @@ -28,32 +35,8 @@ RSpec.describe 'Update work item widgets' do context 'when user has permissions to update a work item', :aggregate_failures do let(:current_user) { developer } - context 'when the updated work item is not valid' do - it 'returns validation errors without the work item' do - errors = ActiveModel::Errors.new(work_item).tap { |e| e.add(:description, 'error message') } - - allow_next_found_instance_of(::WorkItem) do |instance| - allow(instance).to receive(:valid?).and_return(false) - allow(instance).to receive(:errors).and_return(errors) - end - - post_graphql_mutation(mutation, current_user: current_user) - - expect(mutation_response['workItem']).to be_nil - expect(mutation_response['errors']).to match_array(['Description error message']) - end - end - - it 'updates the work item widgets' do - expect do - post_graphql_mutation(mutation, current_user: current_user) - work_item.reload - end.to change(work_item, :description).from(nil).to('updated description') - - expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['workItem']).to include( - 'title' => work_item.title - ) + it_behaves_like 'update work item description widget' do + let(:new_description) { 'updated description' } end it_behaves_like 'has spam protection' do @@ -69,7 +52,7 @@ RSpec.describe 'Update work item widgets' do expect do post_graphql_mutation(mutation, current_user: current_user) work_item.reload - end.to not_change(work_item, :title) + end.to not_change(work_item, :description) expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project') end diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb index bbab6012f3f..01b117a89d8 100644 --- a/spec/requests/api/graphql/project/container_repositories_spec.rb +++ b/spec/requests/api/graphql/project/container_repositories_spec.rb @@ -81,7 +81,7 @@ RSpec.describe 'getting container repositories in a project' do 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 + project.add_member(user, role) unless role == :anonymous end it 'return the proper response' do diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 69e14eace66..596e023a027 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -223,6 +223,7 @@ RSpec.describe 'getting an issue list for a project' do end describe 'sorting and pagination' do + let_it_be(:sort_project) { create(:project, :public) } let_it_be(:data_path) { [:project, :issues] } def pagination_query(params) @@ -237,8 +238,38 @@ RSpec.describe 'getting an issue list for a project' do data.map { |issue| issue['iid'].to_i } end + context 'when sorting by severity' do + let_it_be(:severty_issue1) { create(:issue, project: sort_project) } + let_it_be(:severty_issue2) { create(:issue, project: sort_project) } + let_it_be(:severty_issue3) { create(:issue, project: sort_project) } + let_it_be(:severty_issue4) { create(:issue, project: sort_project) } + let_it_be(:severty_issue5) { create(:issue, project: sort_project) } + + before(:all) do + create(:issuable_severity, issue: severty_issue1, severity: :unknown) + create(:issuable_severity, issue: severty_issue2, severity: :low) + create(:issuable_severity, issue: severty_issue4, severity: :critical) + create(:issuable_severity, issue: severty_issue5, severity: :high) + end + + context 'when ascending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :SEVERITY_ASC } + let(:first_param) { 2 } + let(:all_records) { [severty_issue3.iid, severty_issue1.iid, severty_issue2.iid, severty_issue5.iid, severty_issue4.iid] } + end + end + + context 'when descending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :SEVERITY_DESC } + let(:first_param) { 2 } + let(:all_records) { [severty_issue4.iid, severty_issue5.iid, severty_issue2.iid, severty_issue1.iid, severty_issue3.iid] } + end + end + end + context 'when sorting by due date' do - let_it_be(:sort_project) { create(:project, :public) } let_it_be(:due_issue1) { create(:issue, project: sort_project, due_date: 3.days.from_now) } let_it_be(:due_issue2) { create(:issue, project: sort_project, due_date: nil) } let_it_be(:due_issue3) { create(:issue, project: sort_project, due_date: 2.days.ago) } @@ -263,7 +294,6 @@ RSpec.describe 'getting an issue list for a project' do end context 'when sorting by relative position' do - let_it_be(:sort_project) { create(:project, :public) } let_it_be(:relative_issue1) { create(:issue, project: sort_project, relative_position: 2000) } let_it_be(:relative_issue2) { create(:issue, project: sort_project, relative_position: nil) } let_it_be(:relative_issue3) { create(:issue, project: sort_project, relative_position: 1000) } @@ -285,7 +315,6 @@ RSpec.describe 'getting an issue list for a project' do end context 'when sorting by priority' do - let_it_be(:sort_project) { create(:project, :public) } let_it_be(:on_project) { { project: sort_project } } let_it_be(:early_milestone) { create(:milestone, **on_project, due_date: 10.days.from_now) } let_it_be(:late_milestone) { create(:milestone, **on_project, due_date: 30.days.from_now) } @@ -321,7 +350,6 @@ RSpec.describe 'getting an issue list for a project' do end context 'when sorting by label priority' do - let_it_be(:sort_project) { create(:project, :public) } let_it_be(:label1) { create(:label, project: sort_project, priority: 1) } let_it_be(:label2) { create(:label, project: sort_project, priority: 5) } let_it_be(:label3) { create(:label, project: sort_project, priority: 10) } @@ -348,7 +376,6 @@ RSpec.describe 'getting an issue list for a project' do end context 'when sorting by milestone due date' do - let_it_be(:sort_project) { create(:project, :public) } let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) } let_it_be(:late_milestone) { create(:milestone, project: sort_project, due_date: 30.days.from_now) } let_it_be(:milestone_issue1) { create(:issue, project: sort_project) } diff --git a/spec/requests/api/graphql/project/jobs_spec.rb b/spec/requests/api/graphql/project/jobs_spec.rb index 1a823ede9ac..7d0eb203d60 100644 --- a/spec/requests/api/graphql/project/jobs_spec.rb +++ b/spec/requests/api/graphql/project/jobs_spec.rb @@ -31,8 +31,8 @@ RSpec.describe 'Query.project.jobs' do end it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do - build_stage = create(:ci_stage_entity, position: 1, name: 'build', project: project, pipeline: pipeline) - test_stage = create(:ci_stage_entity, position: 2, name: 'test', project: project, pipeline: pipeline) + build_stage = create(:ci_stage, position: 1, name: 'build', project: project, pipeline: pipeline) + test_stage = create(:ci_stage, position: 2, name: 'test', project: project, pipeline: pipeline) create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 1 2', stage: build_stage) create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 2 2', stage: build_stage) create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 1 2', stage: test_stage) diff --git a/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb b/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb index a025c57d4b8..33e1dbcba27 100644 --- a/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb +++ b/spec/requests/api/graphql/project/packages_cleanup_policy_spec.rb @@ -61,7 +61,7 @@ RSpec.describe 'getting the packages cleanup policy linked to a project' do with_them do before do project.update!(visibility: visibility.to_s) - project.add_user(current_user, role) unless role == :anonymous + project.add_member(current_user, role) unless role == :anonymous end it 'return the proper response' do diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb index ccf97918021..08c6a2d9927 100644 --- a/spec/requests/api/graphql/project/pipeline_spec.rb +++ b/spec/requests/api/graphql/project/pipeline_spec.rb @@ -105,6 +105,62 @@ RSpec.describe 'getting pipeline information nested in a project' do end end + context 'when a job has been retried' do + let_it_be(:retried) do + create(:ci_build, :retried, + name: build_job.name, + pipeline: pipeline, + stage_idx: 0, + stage: build_job.stage) + end + + let(:fields) do + query_graphql_field(:jobs, { retried: retried_argument }, + query_graphql_field(:nodes, {}, all_graphql_fields_for('CiJob', max_depth: 3))) + end + + context 'when we filter out retried jobs' do + let(:retried_argument) { false } + + it 'contains latest jobs' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(*path, :jobs, :nodes)).to include( + a_graphql_entity_for(build_job, :name, :duration, :retried) + ) + + expect(graphql_data_at(*path, :jobs, :nodes)).not_to include( + a_graphql_entity_for(retried) + ) + end + end + + context 'when we filter to only retried jobs' do + let(:retried_argument) { true } + + it 'contains only retried jobs' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(*path, :jobs, :nodes)).to contain_exactly( + a_graphql_entity_for(retried) + ) + end + end + + context 'when we pass null explicitly' do + let(:retried_argument) { nil } + + it 'contains all jobs' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(*path, :jobs, :nodes)).to include( + a_graphql_entity_for(build_job), + a_graphql_entity_for(retried) + ) + end + end + end + context 'when requesting only builds with certain statuses' do let(:variables) do { @@ -290,8 +346,8 @@ RSpec.describe 'getting pipeline information nested in a project' do end it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do - build_stage = create(:ci_stage_entity, position: 1, name: 'build', project: project, pipeline: pipeline) - test_stage = create(:ci_stage_entity, position: 2, name: 'test', project: project, pipeline: pipeline) + build_stage = create(:ci_stage, position: 1, name: 'build', project: project, pipeline: pipeline) + test_stage = create(:ci_stage, position: 2, name: 'test', project: project, pipeline: pipeline) create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 1 2', stage: build_stage) create(:ci_build, pipeline: pipeline, stage_idx: build_stage.position, name: 'docker 2 2', stage: build_stage) create(:ci_build, pipeline: pipeline, stage_idx: test_stage.position, name: 'rspec 1 2', stage: test_stage) diff --git a/spec/requests/api/graphql/project/project_members_spec.rb b/spec/requests/api/graphql/project/project_members_spec.rb index c3281b44954..4225c3ad3e8 100644 --- a/spec/requests/api/graphql/project/project_members_spec.rb +++ b/spec/requests/api/graphql/project/project_members_spec.rb @@ -8,8 +8,8 @@ RSpec.describe 'getting project members information' do let_it_be(:parent_group) { create(:group, :public) } let_it_be(:parent_project) { create(:project, :public, group: parent_group) } let_it_be(:user) { create(:user) } - let_it_be(:user_1) { create(:user, username: 'user') } - let_it_be(:user_2) { create(:user, username: 'test') } + let_it_be(:user_1) { create(:user, username: 'user', name: 'Same Name') } + let_it_be(:user_2) { create(:user, username: 'test', name: 'Same Name') } before_all do [user_1, user_2].each { |user| parent_group.add_guest(user) } @@ -29,11 +29,44 @@ RSpec.describe 'getting project members information' do expect_array_response(user_1, user_2) end - it 'returns members that match the search query' do - fetch_members(project: parent_project, args: { search: 'test' }) + describe 'search argument' do + it 'returns members that match the search query' do + fetch_members(project: parent_project, args: { search: 'test' }) - expect(graphql_errors).to be_nil - expect_array_response(user_2) + expect(graphql_errors).to be_nil + expect_array_response(user_2) + end + + context 'when paginating' do + it 'returns correct results' do + fetch_members(project: parent_project, args: { search: 'Same Name', first: 1 }) + + expect_array_response(user_1) + + next_cursor = graphql_data_at(:project, :projectMembers, :pageInfo, :endCursor) + fetch_members(project: parent_project, args: { search: 'Same Name', first: 1, after: next_cursor }) + + expect_array_response(user_2) + end + + context 'when the use_keyset_aware_user_search_query FF is off' do + before do + stub_feature_flags(use_keyset_aware_user_search_query: false) + end + + it 'raises error on the 2nd page due to missing cursor data' do + fetch_members(project: parent_project, args: { search: 'Same Name', first: 1 }) + + # user_2 because the "old" order was undeterministic (insert order), no tie-breaker column + expect_array_response(user_2) + + next_cursor = graphql_data_at(:project, :projectMembers, :pageInfo, :endCursor) + fetch_members(project: parent_project, args: { search: 'Same Name', first: 1, after: next_cursor }) + + expect(graphql_errors.first['message']).to include('PG::UndefinedColumn') + end + end + end end end @@ -231,6 +264,9 @@ RSpec.describe 'getting project members information' do } } } + pageInfo { + endCursor + } NODE graphql_query_for('project', diff --git a/spec/requests/api/graphql/todo_query_spec.rb b/spec/requests/api/graphql/todo_query_spec.rb new file mode 100644 index 00000000000..3f743f4402a --- /dev/null +++ b/spec/requests/api/graphql/todo_query_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Todo Query' do + include GraphqlHelpers + + let_it_be(:current_user) { nil } + let_it_be(:project) { create(:project) } + let_it_be(:issue) { create(:issue, project: project) } + + let_it_be(:todo_owner) { create(:user) } + + let_it_be(:todo) { create(:todo, user: todo_owner, target: project) } + + before do + project.add_developer(todo_owner) + end + + let(:fields) do + <<~GRAPHQL + id + GRAPHQL + end + + let(:query) do + graphql_query_for(:todo, { id: todo.to_global_id.to_s }, fields) + end + + subject do + result = GitlabSchema.execute(query, context: { current_user: current_user }).to_h + graphql_dig_at(result, :data, :todo) + end + + context 'when requesting user is todo owner' do + let(:current_user) { todo_owner } + + it { is_expected.to include('id' => todo.to_global_id.to_s) } + end + + context 'when requesting user is not todo owner' do + let(:current_user) { create(:user) } + + it { is_expected.to be_nil } + end + + context 'when unauthenticated' do + it { is_expected.to be_nil } + end +end diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index 09bda8ee0d5..f17d2ebbb7e 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -8,7 +8,7 @@ RSpec.describe 'Query.work_item(id)' do let_it_be(:developer) { create(:user) } let_it_be(:guest) { create(:user) } let_it_be(:project) { create(:project, :private) } - let_it_be(:work_item) { create(:work_item, project: project, description: '- List item') } + let_it_be(:work_item) { create(:work_item, project: project, description: '- List item', weight: 1) } let_it_be(:child_item1) { create(:work_item, :task, project: project) } let_it_be(:child_item2) { create(:work_item, :task, confidential: true, project: project) } let_it_be(:child_link1) { create(:parent_link, work_item_parent: work_item, work_item: child_item1) } @@ -64,16 +64,13 @@ RSpec.describe 'Query.work_item(id)' do it 'returns widget information' do expect(work_item_data).to include( 'id' => work_item.to_gid.to_s, - 'widgets' => match_array([ + 'widgets' => include( hash_including( 'type' => 'DESCRIPTION', 'description' => work_item.description, 'descriptionHtml' => ::MarkupHelper.markdown_field(work_item, :description, {}) - ), - hash_including( - 'type' => 'HIERARCHY' ) - ]) + ) ) end end @@ -101,10 +98,7 @@ RSpec.describe 'Query.work_item(id)' do it 'returns widget information' do expect(work_item_data).to include( 'id' => work_item.to_gid.to_s, - 'widgets' => match_array([ - hash_including( - 'type' => 'DESCRIPTION' - ), + 'widgets' => include( hash_including( 'type' => 'HIERARCHY', 'parent' => nil, @@ -113,7 +107,7 @@ RSpec.describe 'Query.work_item(id)' do hash_including('id' => child_link2.work_item.to_gid.to_s) ]) } ) - ]) + ) ) end @@ -137,10 +131,7 @@ RSpec.describe 'Query.work_item(id)' do it 'filters out not accessible children or parent' do expect(work_item_data).to include( 'id' => work_item.to_gid.to_s, - 'widgets' => match_array([ - hash_including( - 'type' => 'DESCRIPTION' - ), + 'widgets' => include( hash_including( 'type' => 'HIERARCHY', 'parent' => nil, @@ -148,7 +139,7 @@ RSpec.describe 'Query.work_item(id)' do hash_including('id' => child_link1.work_item.to_gid.to_s) ]) } ) - ]) + ) ) end end @@ -160,20 +151,85 @@ RSpec.describe 'Query.work_item(id)' do it 'returns parent information' do expect(work_item_data).to include( 'id' => work_item.to_gid.to_s, - 'widgets' => match_array([ - hash_including( - 'type' => 'DESCRIPTION' - ), + 'widgets' => include( hash_including( 'type' => 'HIERARCHY', 'parent' => hash_including('id' => parent_link.work_item_parent.to_gid.to_s), 'children' => { 'nodes' => match_array([]) } ) - ]) + ) ) end end end + + describe 'weight widget' do + let(:work_item_fields) do + <<~GRAPHQL + id + widgets { + type + ... on WorkItemWidgetWeight { + weight + } + } + GRAPHQL + end + + it 'returns widget information' do + expect(work_item_data).to include( + 'id' => work_item.to_gid.to_s, + 'widgets' => include( + hash_including( + 'type' => 'WEIGHT', + 'weight' => work_item.weight + ) + ) + ) + end + end + + describe 'assignees widget' do + let(:assignees) { create_list(:user, 2) } + let(:work_item) { create(:work_item, project: project, assignees: assignees) } + + let(:work_item_fields) do + <<~GRAPHQL + id + widgets { + type + ... on WorkItemWidgetAssignees { + allowsMultipleAssignees + canInviteMembers + assignees { + nodes { + id + username + } + } + } + } + GRAPHQL + end + + it 'returns widget information' do + expect(work_item_data).to include( + 'id' => work_item.to_gid.to_s, + 'widgets' => include( + hash_including( + 'type' => 'ASSIGNEES', + 'allowsMultipleAssignees' => boolean, + 'canInviteMembers' => boolean, + 'assignees' => { + 'nodes' => match_array( + assignees.map { |a| { 'id' => a.to_gid.to_s, 'username' => a.username } } + ) + } + ) + ) + ) + end + end end context 'when an Issue Global ID is provided' do diff --git a/spec/requests/api/group_export_spec.rb b/spec/requests/api/group_export_spec.rb index ffa313d4464..bda46f85140 100644 --- a/spec/requests/api/group_export_spec.rb +++ b/spec/requests/api/group_export_spec.rb @@ -32,9 +32,9 @@ RSpec.describe API::GroupExport do context 'when export file exists' do before do - allow(Gitlab::ApplicationRateLimiter) - .to receive(:increment) - .and_return(0) + allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy| + allow(strategy).to receive(:increment).and_return(0) + end upload.export_file = fixture_file_upload('spec/fixtures/group_export.tar.gz', "`/tar.gz") upload.save! @@ -149,9 +149,11 @@ RSpec.describe API::GroupExport do before do group.add_owner(user) - allow(Gitlab::ApplicationRateLimiter) - .to receive(:increment) - .and_return(Gitlab::ApplicationRateLimiter.rate_limits[:group_export][:threshold].call + 1) + allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy| + allow(strategy) + .to receive(:increment) + .and_return(Gitlab::ApplicationRateLimiter.rate_limits[:group_export][:threshold].call + 1) + end end it 'throttles the endpoint' do diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb index 6d5676bbe35..a7b4bea362f 100644 --- a/spec/requests/api/group_variables_spec.rb +++ b/spec/requests/api/group_variables_spec.rb @@ -10,7 +10,7 @@ RSpec.describe API::GroupVariables do let(:access_level) {} before do - group.add_user(user, access_level) if access_level + group.add_member(user, access_level) if access_level end describe 'GET /groups/:id/variables' do diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 56f08249bdd..3bc3cce5310 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -645,7 +645,7 @@ RSpec.describe API::Groups do project = create(:project, namespace: group2, path: 'Foo') create(:project_group_link, project: project, group: group1) - get api("/groups/#{group1.id}", user1), params: { with_projects: false } + get api("/groups/#{group2.id}", user1), params: { with_projects: false } expect(response).to have_gitlab_http_status(:ok) expect(json_response['projects']).to be_nil @@ -748,6 +748,18 @@ RSpec.describe API::Groups do expect(json_response).to include('runners_token') end + it "returns runners_token and no projects when with_projects option is set to false" do + project = create(:project, namespace: group2, path: 'Foo') + create(:project_group_link, project: project, group: group1) + + get api("/groups/#{group2.id}", admin), params: { with_projects: false } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['projects']).to be_nil + expect(json_response['shared_projects']).to be_nil + expect(json_response).to include('runners_token') + end + it "does not return a non existing group" do get api("/groups/#{non_existing_record_id}", admin) diff --git a/spec/requests/api/integrations_spec.rb b/spec/requests/api/integrations_spec.rb index cd9a0746581..b2db7f7caef 100644 --- a/spec/requests/api/integrations_spec.rb +++ b/spec/requests/api/integrations_spec.rb @@ -55,25 +55,20 @@ RSpec.describe API::Integrations do describe "PUT /projects/:id/#{endpoint}/#{integration.dasherize}" do include_context integration - # NOTE: Some attributes are not supported for PUT requests, even though in most cases they should be. - # For some of them the problem is somewhere else, i.e. most chat integrations don't support the `*_channel` - # fields but they're incorrectly included in `#fields`. - # + # NOTE: Some attributes are not supported for PUT requests, even though they probably should be. # We can fix these manually, or with a generic approach like https://gitlab.com/gitlab-org/gitlab/-/issues/348208 - let(:missing_channel_attributes) { %i[push_channel issue_channel confidential_issue_channel merge_request_channel note_channel confidential_note_channel tag_push_channel pipeline_channel wiki_page_channel] } let(:missing_attributes) do { datadog: %i[archive_trace_events], - discord: missing_channel_attributes + %i[branches_to_be_notified notify_only_broken_pipelines], - hangouts_chat: missing_channel_attributes + %i[notify_only_broken_pipelines], + discord: %i[branches_to_be_notified notify_only_broken_pipelines], + hangouts_chat: %i[notify_only_broken_pipelines], jira: %i[issues_enabled project_key vulnerabilities_enabled vulnerabilities_issuetype], mattermost: %i[deployment_channel labels_to_be_notified], - microsoft_teams: missing_channel_attributes, mock_ci: %i[enable_ssl_verification], prometheus: %i[manual_configuration], slack: %i[alert_events alert_channel deployment_channel labels_to_be_notified], - unify_circuit: missing_channel_attributes + %i[branches_to_be_notified notify_only_broken_pipelines], - webex_teams: missing_channel_attributes + %i[branches_to_be_notified notify_only_broken_pipelines] + unify_circuit: %i[branches_to_be_notified notify_only_broken_pipelines], + webex_teams: %i[branches_to_be_notified notify_only_broken_pipelines] } end @@ -368,6 +363,31 @@ RSpec.describe API::Integrations do end end + describe 'Jira integration' do + let(:integration_name) { 'jira' } + let(:params) do + { url: 'https://jira.example.com', username: 'username', password: 'password' } + end + + before do + project.create_jira_integration(active: true, properties: params) + end + + it 'returns the jira_issue_transition_id for get request' do + get api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['properties']).to include('jira_issue_transition_id' => nil) + end + + it 'returns the jira_issue_transition_id for put request' do + put api("/projects/#{project.id}/#{endpoint}/#{integration_name}", user), params: params.merge(jira_issue_transition_id: '1') + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['properties']['jira_issue_transition_id']).to eq('1') + end + end + describe 'Pipelines Email Integration' do let(:integration_name) { 'pipelines-email' } diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index 93e4e72f78f..acfe476a864 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -51,64 +51,6 @@ RSpec.describe API::Internal::Base do end end - describe 'GET /internal/error_tracking_allowed' do - let_it_be(:project) { create(:project) } - - let(:params) { { project_id: project.id, public_key: 'key' } } - - context 'when the secret header is missing' do - it 'responds with unauthorized entity' do - post api("/internal/error_tracking_allowed"), params: params - - expect(response).to have_gitlab_http_status(:unauthorized) - end - end - - context 'when some params are missing' do - it 'responds with unprocessable entity' do - post api("/internal/error_tracking_allowed"), params: params.except(:public_key), - headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) } - - expect(response).to have_gitlab_http_status(:unprocessable_entity) - end - end - - context 'when the error tracking is disabled' do - it 'returns enabled: false' do - create(:error_tracking_client_key, project: project, active: false) - - post api("/internal/error_tracking_allowed"), params: params, - headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'enabled' => false }) - end - - context 'when the error tracking record does not exist' do - it 'returns enabled: false' do - post api("/internal/error_tracking_allowed"), params: params, - headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'enabled' => false }) - end - end - end - - context 'when the error tracking is enabled' do - it 'returns enabled: true' do - client_key = create(:error_tracking_client_key, project: project, active: true) - params[:public_key] = client_key.public_key - - post api("/internal/error_tracking_allowed"), params: params, - headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq({ 'enabled' => true }) - end - end - end - describe 'GET /internal/two_factor_recovery_codes' do let(:key_id) { key.id } diff --git a/spec/requests/api/internal/error_tracking_spec.rb b/spec/requests/api/internal/error_tracking_spec.rb new file mode 100644 index 00000000000..69eb54d5ed2 --- /dev/null +++ b/spec/requests/api/internal/error_tracking_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Internal::ErrorTracking do + let(:secret_token) { Gitlab::CurrentSettings.error_tracking_access_token } + let(:headers) do + { ::API::Internal::ErrorTracking::GITLAB_ERROR_TRACKING_TOKEN_HEADER => Base64.encode64(secret_token) } + end + + describe 'GET /internal/error_tracking/allowed' do + let_it_be(:project) { create(:project) } + + let(:params) { { project_id: project.id, public_key: 'key' } } + + subject(:send_request) do + post api('/internal/error_tracking/allowed'), params: params, headers: headers + end + + before do + # Because the feature flag is disabled in specs we have to enable it explicitly. + stub_feature_flags(use_click_house_database_for_error_tracking: true) + stub_feature_flags(gitlab_error_tracking: true) + end + + context 'when the secret header is missing' do + let(:headers) { {} } + + it 'responds with unauthorized entity' do + send_request + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'when some params are missing' do + let(:params) { { project_id: project.id } } + + it 'responds with unprocessable entity' do + send_request + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end + + context 'when public_key is unknown' do + it 'returns enabled: false' do + send_request + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq('enabled' => false) + end + end + + context 'when unknown project_id is unknown' do + it 'responds with 404 not found' do + params[:project_id] = non_existing_record_id + + send_request + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when the error tracking is disabled' do + it 'returns enabled: false' do + create(:error_tracking_client_key, :disabled, project: project) + + send_request + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq('enabled' => false) + end + end + + context 'when the error tracking is enabled' do + let_it_be(:client_key) { create(:error_tracking_client_key, project: project) } + + before do + params[:public_key] = client_key.public_key + + stub_application_setting(error_tracking_enabled: true) + stub_application_setting(error_tracking_api_url: 'https://localhost/error_tracking') + end + + it 'returns enabled: true' do + send_request + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq('enabled' => true) + end + + context 'when feature flags use_click_house_database_for_error_tracking or gitlab_error_tracking are disabled' do + before do + stub_feature_flags(use_click_house_database_for_error_tracking: false) + stub_feature_flags(gitlab_error_tracking: false) + end + + it 'returns enabled: false' do + send_request + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq('enabled' => false) + end + end + end + end +end diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb index 0e566dd8c0e..c0a979995c9 100644 --- a/spec/requests/api/internal/kubernetes_spec.rb +++ b/spec/requests/api/internal/kubernetes_spec.rb @@ -169,12 +169,12 @@ RSpec.describe API::Internal::Kubernetes do 'features' => {} ), 'gitaly_repository' => a_hash_including( - 'default_branch' => project.default_branch_or_main, 'storage_name' => project.repository_storage, 'relative_path' => project.disk_path + '.git', 'gl_repository' => "project-#{project.id}", 'gl_project_path' => project.full_path - ) + ), + 'default_branch' => project.default_branch_or_main ) ) end diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb index 64ad5733c1b..53154aef21e 100644 --- a/spec/requests/api/invitations_spec.rb +++ b/spec/requests/api/invitations_spec.rb @@ -69,6 +69,20 @@ RSpec.describe API::Invitations do end end + context 'when invitee is already an invited member' do + it 'updates the member for that email' do + member = source.add_developer(email) + + expect do + post invitations_url(source, maintainer), + params: { email: email, access_level: Member::MAINTAINER } + + expect(response).to have_gitlab_http_status(:created) + end.to change { member.reset.access_level }.from(Member::DEVELOPER).to(Member::MAINTAINER) + .and not_change { source.members.invite.count } + end + end + it 'adds a new member by email' do expect do post invitations_url(source, maintainer), @@ -320,7 +334,7 @@ RSpec.describe API::Invitations do let(:source) { project } end - it 'records queries', :request_store, :use_sql_query_cache do + it 'does not exceed expected queries count for emails', :request_store, :use_sql_query_cache do post invitations_url(project, maintainer), params: { email: email, access_level: Member::DEVELOPER } control = ActiveRecord::QueryRecorder.new(skip_cached: false) do @@ -336,7 +350,25 @@ RSpec.describe API::Invitations do end.not_to exceed_all_query_limit(control.count).with_threshold(unresolved_n_plus_ones) end - it 'records queries with secondary emails', :request_store, :use_sql_query_cache do + it 'does not exceed expected queries count for user_ids', :request_store, :use_sql_query_cache do + stranger2 = create(:user) + + post invitations_url(project, maintainer), params: { user_id: stranger.id, access_level: Member::DEVELOPER } + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post invitations_url(project, maintainer), params: { user_id: stranger2.id, access_level: Member::DEVELOPER } + end + + users = create_list(:user, 5) + + unresolved_n_plus_ones = 136 # 54 for 1 vs 190 for 5 - currently there are 34 queries added per user + + expect do + post invitations_url(project, maintainer), params: { user_id: users.map(&:id).join(','), access_level: Member::DEVELOPER } + end.not_to exceed_all_query_limit(control.count).with_threshold(unresolved_n_plus_ones) + end + + it 'does not exceed expected queries count with secondary emails', :request_store, :use_sql_query_cache do create(:email, email: email, user: create(:user)) post invitations_url(project, maintainer), params: { email: email, access_level: Member::DEVELOPER } @@ -365,7 +397,7 @@ RSpec.describe API::Invitations do let(:source) { group } end - it 'records queries', :request_store, :use_sql_query_cache do + it 'does not exceed expected queries count for emails', :request_store, :use_sql_query_cache do post invitations_url(group, maintainer), params: { email: email, access_level: Member::DEVELOPER } control = ActiveRecord::QueryRecorder.new(skip_cached: false) do @@ -381,7 +413,7 @@ RSpec.describe API::Invitations do end.not_to exceed_all_query_limit(control.count).with_threshold(unresolved_n_plus_ones) end - it 'records queries with secondary emails', :request_store, :use_sql_query_cache do + it 'does not exceed expected queries count for secondary emails', :request_store, :use_sql_query_cache do create(:email, email: email, user: create(:user)) post invitations_url(group, maintainer), params: { email: email, access_level: Member::DEVELOPER } diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb index 480baff6eed..dd7d32f3565 100644 --- a/spec/requests/api/issues/issues_spec.rb +++ b/spec/requests/api/issues/issues_spec.rb @@ -20,6 +20,7 @@ RSpec.describe API::Issues do let_it_be(:milestone) { create(:milestone, title: '1.0.0', project: project) } let_it_be(:empty_milestone) { create(:milestone, title: '2.0.0', project: project) } + let_it_be(:task) { create(:issue, :task, author: user, project: project) } let_it_be(:closed_issue) do create :closed_issue, @@ -1151,19 +1152,6 @@ RSpec.describe API::Issues do expected_url = expose_url(api_v4_project_issue_path(id: new_issue.project_id, issue_iid: new_issue.iid)) expect(json_response.dig('_links', 'closed_as_duplicate_of')).to eq(expected_url) end - - context 'feature flag is disabled' do - before do - stub_feature_flags(closed_as_duplicate_of_issues_api: false) - end - - it 'does not return the issue as closed_as_duplicate_of' do - get api("/projects/#{project.id}/issues/#{issue_closed_as_dup.iid}", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.dig('_links', 'closed_as_duplicate_of')).to eq(nil) - end - end end end end diff --git a/spec/requests/api/markdown_snapshot_spec.rb b/spec/requests/api/markdown_snapshot_spec.rb index 37607a4e866..1270efdfd6f 100644 --- a/spec/requests/api/markdown_snapshot_spec.rb +++ b/spec/requests/api/markdown_snapshot_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' # See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing # for documentation on this spec. RSpec.describe API::Markdown, 'Snapshot' do + # noinspection RubyMismatchedArgumentType (ignore RBS type warning: __dir__ can be nil, but 2nd argument can't be nil) glfm_specification_dir = File.expand_path('../../../glfm_specification', __dir__) - glfm_example_snapshots_dir = File.expand_path('../../fixtures/glfm/example_snapshots', __dir__) - include_context 'with API::Markdown Snapshot shared context', glfm_specification_dir, glfm_example_snapshots_dir + include_context 'with API::Markdown Snapshot shared context', glfm_specification_dir end diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb index bc325aad823..ba82d2facc6 100644 --- a/spec/requests/api/maven_packages_spec.rb +++ b/spec/requests/api/maven_packages_spec.rb @@ -226,14 +226,26 @@ RSpec.describe API::MavenPackages do end end + shared_examples 'file download in FIPS mode' do + context 'in FIPS mode', :fips_mode do + it_behaves_like 'successfully returning the file' + + it 'rejects the request for an md5 file' do + download_file(file_name: package_file.file_name + '.md5') + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end + end + describe 'GET /api/v4/packages/maven/*path/:file_name' do context 'a public project' do subject { download_file(file_name: package_file.file_name) } shared_examples 'getting a file' do it_behaves_like 'tracking the file download event' - it_behaves_like 'successfully returning the file' + it_behaves_like 'file download in FIPS mode' it 'returns sha1 of the file' do download_file(file_name: package_file.file_name + '.sha1') @@ -402,8 +414,8 @@ RSpec.describe API::MavenPackages do shared_examples 'getting a file for a group' do it_behaves_like 'tracking the file download event' - it_behaves_like 'successfully returning the file' + it_behaves_like 'file download in FIPS mode' it 'returns sha1 of the file' do download_file(file_name: package_file.file_name + '.sha1') @@ -625,8 +637,8 @@ RSpec.describe API::MavenPackages do subject { download_file(file_name: package_file.file_name) } it_behaves_like 'tracking the file download event' - it_behaves_like 'successfully returning the file' + it_behaves_like 'file download in FIPS mode' it 'returns sha1 of the file' do download_file(file_name: package_file.file_name + '.sha1') @@ -833,6 +845,16 @@ RSpec.describe API::MavenPackages do subject { upload_file_with_token(params: params) } + context 'FIPS mode', :fips_mode do + it_behaves_like 'package workhorse uploads' + + it 'rejects the request for md5 file' do + upload_file_with_token(params: params, file_extension: 'jar.md5') + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end + context 'file size is too large' do it 'rejects the request' do allow_next_instance_of(UploadedFile) do |uploaded_file| @@ -995,12 +1017,22 @@ RSpec.describe API::MavenPackages do end context 'for md5 file' do + subject { upload_file_with_token(params: params, file_extension: 'jar.md5') } + it 'returns an empty body' do - upload_file_with_token(params: params, file_extension: 'jar.md5') + subject expect(response.body).to eq('') expect(response).to have_gitlab_http_status(:ok) end + + context 'with FIPS mode enabled', :fips_mode do + it 'rejects the request' do + subject + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end end end diff --git a/spec/requests/api/metadata_spec.rb b/spec/requests/api/metadata_spec.rb new file mode 100644 index 00000000000..dbca06b7f3e --- /dev/null +++ b/spec/requests/api/metadata_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Metadata do + shared_examples_for 'GET /metadata' do + context 'when unauthenticated' do + it 'returns authentication error' do + get api('/metadata') + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'when authenticated as user' do + let(:user) { create(:user) } + + it 'returns the metadata information' do + get api('/metadata', user) + + expect_metadata + end + end + + context 'when authenticated with token' do + let(:personal_access_token) { create(:personal_access_token, scopes: scopes) } + + context 'with api scope' do + let(:scopes) { %i(api) } + + it 'returns the metadata information' do + get api('/metadata', personal_access_token: personal_access_token) + + expect_metadata + end + + it 'returns "200" response on head requests' do + head api('/metadata', personal_access_token: personal_access_token) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'with read_user scope' do + let(:scopes) { %i(read_user) } + + it 'returns the metadata information' do + get api('/metadata', personal_access_token: personal_access_token) + + expect_metadata + end + + it 'returns "200" response on head requests' do + head api('/metadata', personal_access_token: personal_access_token) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'with neither api nor read_user scope' do + let(:scopes) { %i(read_repository) } + + it 'returns authorization error' do + get api('/metadata', personal_access_token: personal_access_token) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + def expect_metadata + aggregate_failures("testing response") do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/metadata') + end + end + end + + context 'with graphql enabled' do + before do + stub_feature_flags(graphql: true) + end + + include_examples 'GET /metadata' + end + + context 'with graphql disabled' do + before do + stub_feature_flags(graphql: false) + end + + include_examples 'GET /metadata' + end +end diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb index 7c3f1890095..62809b432af 100644 --- a/spec/requests/api/npm_project_packages_spec.rb +++ b/spec/requests/api/npm_project_packages_spec.rb @@ -30,6 +30,7 @@ RSpec.describe API::NpmProjectPackages do end describe 'GET /api/v4/projects/:id/packages/npm/*package_name/-/*file_name' do + let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } } let(:package_file) { package.package_files.first } let(:headers) { {} } @@ -61,18 +62,18 @@ RSpec.describe API::NpmProjectPackages do let(:headers) { build_token_auth_header(token.token) } it_behaves_like 'successfully downloads the file' + it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package' end context 'with job token' do let(:headers) { build_token_auth_header(job.token) } it_behaves_like 'successfully downloads the file' + it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package' end end context 'a public project' do - let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } } - it_behaves_like 'successfully downloads the file' it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package' @@ -112,6 +113,15 @@ RSpec.describe API::NpmProjectPackages do end it_behaves_like 'a package file that requires auth' + + context 'with a job token for a different user' do + let_it_be(:other_user) { create(:user) } + let_it_be_with_reload(:other_job) { create(:ci_build, :running, user: other_user) } + + let(:headers) { build_token_auth_header(other_job.token) } + + it_behaves_like 'successfully downloads the file' + end end end diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml index 35844631287..8d3622ca17d 100644 --- a/spec/requests/api/project_attributes.yml +++ b/spec/requests/api/project_attributes.yml @@ -100,6 +100,7 @@ ci_cd_settings: forward_deployment_enabled: ci_forward_deployment_enabled job_token_scope_enabled: ci_job_token_scope_enabled separated_caches: ci_separated_caches + opt_in_jwt: ci_opt_in_jwt build_import_state: # import_state unexposed_attributes: @@ -123,6 +124,11 @@ project_feature: - created_at - metrics_dashboard_access_level - package_registry_access_level + - monitor_access_level + - infrastructure_access_level + - feature_flags_access_level + - environments_access_level + - releases_access_level - project_id - updated_at computed_attributes: diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb index 8a8cd8512f8..d74fd82ca09 100644 --- a/spec/requests/api/project_export_spec.rb +++ b/spec/requests/api/project_export_spec.rb @@ -248,9 +248,10 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache do let(:request) { get api(download_path, admin) } before do - allow(Gitlab::ApplicationRateLimiter) - .to receive(:increment) - .and_return(Gitlab::ApplicationRateLimiter.rate_limits[:project_download_export][:threshold].call + 1) + allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy| + threshold = Gitlab::ApplicationRateLimiter.rate_limits[:project_download_export][:threshold].call + allow(strategy).to receive(:increment).and_return(threshold + 1) + end end it 'prevents requesting project export' do @@ -433,9 +434,10 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache do context 'when rate limit is exceeded across projects' do before do - allow(Gitlab::ApplicationRateLimiter) - .to receive(:increment) - .and_return(Gitlab::ApplicationRateLimiter.rate_limits[:project_export][:threshold].call + 1) + allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy| + threshold = Gitlab::ApplicationRateLimiter.rate_limits[:project_export][:threshold].call + allow(strategy).to receive(:increment).and_return(threshold + 1) + end end it 'prevents requesting project export' do diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index 26e0adc11b3..2d925620a91 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -3,10 +3,10 @@ require 'spec_helper' RSpec.describe API::ProjectHooks, 'ProjectHooks' do - let(:user) { create(:user) } - let(:user3) { create(:user) } - let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - let!(:hook) do + let_it_be(:user) { create(:user) } + let_it_be(:user3) { create(:user) } + let_it_be(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } + let_it_be_with_refind(:hook) do create(:project_hook, :all_events_enabled, project: project, @@ -15,232 +15,55 @@ RSpec.describe API::ProjectHooks, 'ProjectHooks' do push_events_branch_filter: 'master') end - before do + before_all do project.add_maintainer(user) project.add_developer(user3) end - describe "GET /projects/:id/hooks" do - context "authorized user" do - it "returns project hooks" do - get api("/projects/#{project.id}/hooks", user) + it_behaves_like 'web-hook API endpoints', '/projects/:id' do + let(:unauthorized_user) { user3 } - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to be_an Array - expect(response).to include_pagination_headers - expect(json_response.count).to eq(1) - expect(json_response.first['url']).to eq("http://example.com") - expect(json_response.first['issues_events']).to eq(true) - expect(json_response.first['confidential_issues_events']).to eq(true) - expect(json_response.first['push_events']).to eq(true) - expect(json_response.first['merge_requests_events']).to eq(true) - expect(json_response.first['tag_push_events']).to eq(true) - expect(json_response.first['note_events']).to eq(true) - expect(json_response.first['confidential_note_events']).to eq(true) - expect(json_response.first['job_events']).to eq(true) - 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') - expect(json_response.first['alert_status']).to eq('executable') - expect(json_response.first['disabled_until']).to be_nil - end + def scope + project.hooks end - context "unauthorized user" do - it "does not access project hooks" do - get api("/projects/#{project.id}/hooks", user3) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - end - - describe "GET /projects/:id/hooks/:hook_id" do - context "authorized user" do - it "returns a project hook" do - get api("/projects/#{project.id}/hooks/#{hook.id}", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['url']).to eq(hook.url) - expect(json_response['issues_events']).to eq(hook.issues_events) - expect(json_response['confidential_issues_events']).to eq(hook.confidential_issues_events) - expect(json_response['push_events']).to eq(hook.push_events) - expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) - expect(json_response['tag_push_events']).to eq(hook.tag_push_events) - expect(json_response['note_events']).to eq(hook.note_events) - expect(json_response['confidential_note_events']).to eq(hook.confidential_note_events) - 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) - expect(json_response['alert_status']).to eq(hook.alert_status.to_s) - expect(json_response['disabled_until']).to be_nil - end - - it "returns a 404 error if hook id is not available" do - get api("/projects/#{project.id}/hooks/#{non_existing_record_id}", user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context "unauthorized user" do - it "does not access an existing hook" do - get api("/projects/#{project.id}/hooks/#{hook.id}", user3) - expect(response).to have_gitlab_http_status(:forbidden) - end - end - end - - describe "POST /projects/:id/hooks" do - it "adds hook to project" do - expect 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, releases_events: true, - push_events_branch_filter: 'some-feature-branch' }) - end.to change {project.hooks.count}.by(1) - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['url']).to eq('http://example.com') - expect(json_response['issues_events']).to eq(true) - expect(json_response['confidential_issues_events']).to eq(true) - expect(json_response['push_events']).to eq(true) - expect(json_response['merge_requests_events']).to eq(false) - expect(json_response['tag_push_events']).to eq(false) - expect(json_response['note_events']).to eq(false) - expect(json_response['confidential_note_events']).to eq(nil) - expect(json_response['job_events']).to eq(true) - 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') + def collection_uri + "/projects/#{project.id}/hooks" end - it "adds the token without including it in the response" do - token = "secret token" - - expect do - post api("/projects/#{project.id}/hooks", user), params: { url: "http://example.com", token: token } - end.to change {project.hooks.count}.by(1) - - expect(response).to have_gitlab_http_status(:created) - expect(json_response["url"]).to eq("http://example.com") - expect(json_response).not_to include("token") - - hook = project.hooks.find(json_response["id"]) - - expect(hook.url).to eq("http://example.com") - expect(hook.token).to eq(token) + def match_collection_schema + match_response_schema('public_api/v4/project_hooks') end - it "returns a 400 error if url not given" do - post api("/projects/#{project.id}/hooks", user) - expect(response).to have_gitlab_http_status(:bad_request) + def hook_uri(hook_id = hook.id) + "/projects/#{project.id}/hooks/#{hook_id}" end - it "returns a 422 error if url not valid" do - post api("/projects/#{project.id}/hooks", user), params: { url: "ftp://example.com" } - expect(response).to have_gitlab_http_status(:unprocessable_entity) + def match_hook_schema + match_response_schema('public_api/v4/project_hook') end - it "returns a 422 error if branch filter is not valid" do - post api("/projects/#{project.id}/hooks", user), params: { url: "http://example.com", push_events_branch_filter: '~badbranchname/' } - expect(response).to have_gitlab_http_status(:unprocessable_entity) + def event_names + %i[ + push_events + tag_push_events + merge_requests_events + issues_events + confidential_issues_events + note_events + confidential_note_events + pipeline_events + wiki_page_events + job_events + deployment_events + releases_events + ] end - end - - describe "PUT /projects/:id/hooks/:hook_id" do - it "updates an existing project hook" do - put api("/projects/#{project.id}/hooks/#{hook.id}", user), - params: { url: 'http://example.org', push_events: false, job_events: true } - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['url']).to eq('http://example.org') - expect(json_response['issues_events']).to eq(hook.issues_events) - expect(json_response['confidential_issues_events']).to eq(hook.confidential_issues_events) - expect(json_response['push_events']).to eq(false) - expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) - expect(json_response['tag_push_events']).to eq(hook.tag_push_events) - expect(json_response['note_events']).to eq(hook.note_events) - expect(json_response['confidential_note_events']).to eq(hook.confidential_note_events) - 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) + let(:default_values) do + { push_events: true, confidential_note_events: nil } end - it "adds the token without including it in the response" do - token = "secret token" - - put api("/projects/#{project.id}/hooks/#{hook.id}", user), params: { url: "http://example.org", token: token } - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response["url"]).to eq("http://example.org") - expect(json_response).not_to include("token") - - expect(hook.reload.url).to eq("http://example.org") - expect(hook.reload.token).to eq(token) - end - - it "returns 404 error if hook id not found" do - put api("/projects/#{project.id}/hooks/#{non_existing_record_id}", user), params: { url: 'http://example.org' } - expect(response).to have_gitlab_http_status(:not_found) - end - - it "returns 400 error if url is not given" do - put api("/projects/#{project.id}/hooks/#{hook.id}", user) - expect(response).to have_gitlab_http_status(:bad_request) - end - - it "returns a 422 error if url is not valid" do - put api("/projects/#{project.id}/hooks/#{hook.id}", user), params: { url: 'ftp://example.com' } - expect(response).to have_gitlab_http_status(:unprocessable_entity) - end - end - - describe "DELETE /projects/:id/hooks/:hook_id" do - it "deletes hook from project" do - expect do - delete api("/projects/#{project.id}/hooks/#{hook.id}", user) - - expect(response).to have_gitlab_http_status(:no_content) - end.to change {project.hooks.count}.by(-1) - end - - it "returns a 404 error when deleting non existent hook" do - delete api("/projects/#{project.id}/hooks/42", user) - expect(response).to have_gitlab_http_status(:not_found) - end - - it "returns a 404 error if hook id not given" do - delete api("/projects/#{project.id}/hooks", user) - - expect(response).to have_gitlab_http_status(:not_found) - end - - it "returns a 404 if a user attempts to delete project hooks they do not own" do - test_user = create(:user) - other_project = create(:project) - other_project.add_maintainer(test_user) - - delete api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user) - expect(response).to have_gitlab_http_status(:not_found) - expect(WebHook.exists?(hook.id)).to be_truthy - end - - it_behaves_like '412 response' do - let(:request) { api("/projects/#{project.id}/hooks/#{hook.id}", user) } - end + it_behaves_like 'web-hook API endpoints with branch-filter', '/projects/:id' end end diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index 7e6d80c047c..8655e5b0238 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -462,6 +462,16 @@ RSpec.describe API::ProjectImport, :aggregate_failures do expect(json_response).to include('import_status' => 'failed', 'import_error' => 'error') end + + it 'returns the import status if canceled' do + project = create(:project, :import_canceled) + project.add_maintainer(user) + + get api("/projects/#{project.id}/import", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include('import_status' => 'canceled') + end end describe 'POST /projects/import/authorize' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 431d2e56cb5..ae689d7327b 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -328,6 +328,45 @@ RSpec.describe API::Projects do end end + context 'filter by topic_id' do + let_it_be(:topic1) { create(:topic) } + let_it_be(:topic2) { create(:topic) } + + let(:current_user) { user } + + before do + project.topics << topic1 + end + + context 'with id of assigned topic' do + it_behaves_like 'projects response' do + let(:filter) { { topic_id: topic1.id } } + let(:projects) { [project] } + end + end + + context 'with id of unassigned topic' do + it_behaves_like 'projects response' do + let(:filter) { { topic_id: topic2.id } } + let(:projects) { [] } + end + end + + context 'with non-existing topic id' do + it_behaves_like 'projects response' do + let(:filter) { { topic_id: non_existing_record_id } } + let(:projects) { [] } + end + end + + context 'with empty topic id' do + it_behaves_like 'projects response' do + let(:filter) { { topic_id: '' } } + let(:projects) { user_projects } + end + end + end + context 'and with_issues_enabled=true' do it 'only returns projects with issues enabled' do project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) @@ -2388,6 +2427,7 @@ RSpec.describe API::Projects do expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved) expect(json_response['ci_default_git_depth']).to eq(project.ci_default_git_depth) expect(json_response['ci_forward_deployment_enabled']).to eq(project.ci_forward_deployment_enabled) + expect(json_response['ci_separated_caches']).to eq(project.ci_separated_caches) expect(json_response['merge_method']).to eq(project.merge_method.to_s) expect(json_response['squash_option']).to eq(project.squash_option.to_s) expect(json_response['readme_url']).to eq(project.readme_url) @@ -3199,7 +3239,7 @@ RSpec.describe API::Projects do measure_project.add_developer(create(:user)) measure_project.add_developer(create(:user)) # make this 2nd one to find any n+1 - unresolved_n_plus_ones = 21 # 21 queries added per member + unresolved_n_plus_ones = 27 # 27 queries added per member expect do post api("/projects/#{project.id}/import_project_members/#{measure_project.id}", user) @@ -3652,6 +3692,7 @@ RSpec.describe API::Projects do merge_method: 'ff', ci_default_git_depth: 20, ci_forward_deployment_enabled: false, + ci_separated_caches: false, description: 'new description' } put api("/projects/#{project3.id}", user4), params: project_param diff --git a/spec/requests/api/protected_tags_spec.rb b/spec/requests/api/protected_tags_spec.rb index cc7261dafc9..84b7df86f31 100644 --- a/spec/requests/api/protected_tags_spec.rb +++ b/spec/requests/api/protected_tags_spec.rb @@ -3,9 +3,10 @@ require 'spec_helper' RSpec.describe API::ProtectedTags do - let(:user) { create(:user) } - let!(:project) { create(:project, :repository) } - let(:project2) { create(:project, path: 'project2', namespace: user.namespace) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:project2) { create(:project, path: 'project2', namespace: user.namespace) } + let(:protected_name) { 'feature' } let(:tag_name) { protected_name } let!(:protected_tag) do diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb index a24b852cdac..9e0d3780fd8 100644 --- a/spec/requests/api/pypi_packages_spec.rb +++ b/spec/requests/api/pypi_packages_spec.rb @@ -197,7 +197,7 @@ RSpec.describe API::PypiPackages do let(:url) { "/projects/#{project.id}/packages/pypi" } let(:headers) { {} } let(:requires_python) { '>=3.7' } - let(:base_params) { { requires_python: requires_python, version: '1.0.0', name: 'sample-project', sha256_digest: '1' * 64 } } + let(:base_params) { { requires_python: requires_python, version: '1.0.0', name: 'sample-project', sha256_digest: '1' * 64, md5_digest: '1' * 32 } } let(:params) { base_params.merge(content: temp_file(file_name)) } let(:send_rewritten_field) { true } let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user } } @@ -254,6 +254,19 @@ RSpec.describe API::PypiPackages do it_behaves_like 'PyPI package creation', :developer, :created, true end + + context 'without md5_digest' do + let(:token) { personal_access_token.token } + let(:user_headers) { basic_auth_header(user.username, token) } + let(:headers) { user_headers.merge(workhorse_headers) } + let(:params) { base_params.merge(content: temp_file(file_name)) } + + before do + params.delete(:md5_digest) + end + + it_behaves_like 'PyPI package creation', :developer, :created, true, false + end end context 'with required_python too big' do diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index d6d2bd5baf2..cf0165d123f 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -784,6 +784,40 @@ RSpec.describe API::Repositories do expect(json_response['notes']).to be_present end + it 'supports specified config file path' do + spy = instance_spy(Repositories::ChangelogService) + + expect(Repositories::ChangelogService) + .to receive(:new) + .with( + project, + user, + version: '1.0.0', + from: 'foo', + to: 'bar', + date: DateTime.new(2020, 1, 1), + trailer: 'Foo', + config_file: 'specified_changelog_config.yml' + ) + .and_return(spy) + + expect(spy).to receive(:execute).with(commit_to_changelog: false) + + get( + api("/projects/#{project.id}/repository/changelog", user), + params: { + version: '1.0.0', + from: 'foo', + to: 'bar', + date: '2020-01-01', + trailer: 'Foo', + config_file: 'specified_changelog_config.yml' + } + ) + + expect(response).to have_gitlab_http_status(:ok) + end + context 'when previous tag version does not exist' do it_behaves_like '422 response' do let(:request) { get api("/projects/#{project.id}/repository/changelog", user), params: { version: 'v0.0.0' } } @@ -905,5 +939,45 @@ RSpec.describe API::Repositories do expect(response).to have_gitlab_http_status(:unprocessable_entity) expect(json_response['message']).to eq('Failed to generate the changelog: oops') end + + it "support specified config file path" do + spy = instance_spy(Repositories::ChangelogService) + + expect(Repositories::ChangelogService) + .to receive(:new) + .with( + project, + user, + version: '1.0.0', + from: 'foo', + to: 'bar', + date: DateTime.new(2020, 1, 1), + branch: 'kittens', + trailer: 'Foo', + config_file: 'specified_changelog_config.yml', + file: 'FOO.md', + message: 'Commit message' + ) + .and_return(spy) + + allow(spy).to receive(:execute).with(commit_to_changelog: true) + + post( + api("/projects/#{project.id}/repository/changelog", user), + params: { + version: '1.0.0', + from: 'foo', + to: 'bar', + date: '2020-01-01', + branch: 'kittens', + trailer: 'Foo', + config_file: 'specified_changelog_config.yml', + file: 'FOO.md', + message: 'Commit message' + } + ) + + expect(response).to have_gitlab_http_status(:ok) + end end end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index cfda06da8f3..d4a8e591622 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -373,7 +373,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do end end - context "snowplow tracking settings" do + context "snowplow tracking settings", :do_not_stub_snowplow_by_default do let(:settings) do { snowplow_collector_hostname: "snowplow.example.com", diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 13160519996..0ba1011684a 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -9,9 +9,9 @@ RSpec.describe API::Snippets, factory_default: :keep do let_it_be(:user) { create(:user) } let_it_be(:other_user) { create(:user) } - let_it_be(:public_snippet) { create(:personal_snippet, :repository, :public, author: user) } - let_it_be(:private_snippet) { create(:personal_snippet, :repository, :private, author: user) } - let_it_be(:internal_snippet) { create(:personal_snippet, :repository, :internal, author: user) } + let_it_be(:public_snippet) { create(:personal_snippet, :repository, :public, author: user) } + let_it_be_with_refind(:private_snippet) { create(:personal_snippet, :repository, :private, author: user) } + let_it_be(:internal_snippet) { create(:personal_snippet, :repository, :internal, author: user) } let_it_be(:user_token) { create(:personal_access_token, user: user) } let_it_be(:other_user_token) { create(:personal_access_token, user: other_user) } @@ -63,6 +63,23 @@ RSpec.describe API::Snippets, factory_default: :keep do expect(snippet["id"]).not_to eq(public_snippet.id) end end + + context 'filtering snippets by created_after/created_before' do + let_it_be(:private_snippet_before_time_range) { create(:personal_snippet, :repository, :private, author: user, created_at: Time.parse("2021-08-20T00:00:00Z")) } + let_it_be(:private_snippet_in_time_range1) { create(:personal_snippet, :repository, :private, author: user, created_at: Time.parse("2021-08-22T00:00:00Z")) } + let_it_be(:private_snippet_in_time_range2) { create(:personal_snippet, :repository, :private, author: user, created_at: Time.parse("2021-08-24T00:00:00Z")) } + let_it_be(:private_snippet_after_time_range) { create(:personal_snippet, :repository, :private, author: user, created_at: Time.parse("2021-08-26T00:00:00Z")) } + + let(:path) { "/snippets?created_after=2021-08-21T00:00:00Z&created_before=2021-08-25T00:00:00Z" } + + it 'returns snippets available for user in given time range' do + get api(path, personal_access_token: user_token) + + expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( + private_snippet_in_time_range1.id, + private_snippet_in_time_range2.id) + end + end end describe 'GET /snippets/public' do @@ -98,6 +115,21 @@ RSpec.describe API::Snippets, factory_default: :keep do expect(response).to have_gitlab_http_status(:unauthorized) end + + context 'filtering public snippets by created_after/created_before' do + let_it_be(:public_snippet_before_time_range) { create(:personal_snippet, :repository, :public, author: other_user, created_at: Time.parse("2021-08-20T00:00:00Z")) } + let_it_be(:public_snippet_in_time_range) { create(:personal_snippet, :repository, :public, author: other_user, created_at: Time.parse("2021-08-22T00:00:00Z")) } + let_it_be(:public_snippet_after_time_range) { create(:personal_snippet, :repository, :public, author: other_user, created_at: Time.parse("2021-08-24T00:00:00Z")) } + + let(:path) { "/snippets/public?created_after=2021-08-21T00:00:00Z&created_before=2021-08-23T00:00:00Z" } + + it 'returns public snippets available to user in given time range' do + get api(path, personal_access_token: user_token) + + expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly( + public_snippet_in_time_range.id) + end + end end describe 'GET /snippets/:id/raw' do diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index 2460a98129f..0f1dbea2e73 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -3,221 +3,58 @@ require 'spec_helper' RSpec.describe API::SystemHooks do - include StubRequests + let_it_be(:non_admin) { create(:user) } + let_it_be(:admin) { create(:admin) } + let_it_be_with_refind(:hook) { create(:system_hook, url: "http://example.com") } - let(:user) { create(:user) } - let(:admin) { create(:admin) } - let!(:hook) { create(:system_hook, url: "http://example.com") } + it_behaves_like 'web-hook API endpoints', '' do + let(:user) { admin } + let(:unauthorized_user) { non_admin } - before do - stub_full_request(hook.url, method: :post) - end - - describe "GET /hooks" do - context "when no user" do - it "returns authentication error" do - get api("/hooks") - - expect(response).to have_gitlab_http_status(:unauthorized) - end + def scope + SystemHook end - context "when not an admin" do - it "returns forbidden error" do - get api("/hooks", user) - - expect(response).to have_gitlab_http_status(:forbidden) - end + def collection_uri + "/hooks" end - context "when authenticated as admin" do - it "returns an array of hooks" do - get api("/hooks", admin) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(response).to match_response_schema('public_api/v4/system_hooks') - expect(json_response.first).not_to have_key("token") - expect(json_response.first['url']).to eq(hook.url) - expect(json_response.first['push_events']).to be false - expect(json_response.first['tag_push_events']).to be false - expect(json_response.first['merge_requests_events']).to be false - expect(json_response.first['repository_update_events']).to be true - expect(json_response.first['enable_ssl_verification']).to be true - expect(json_response.first['disabled_until']).to be nil - expect(json_response.first['alert_status']).to eq 'executable' - end + def match_collection_schema + match_response_schema('public_api/v4/system_hooks') end - end - describe "GET /hooks/:id" do - context "when no user" do - it "returns authentication error" do - get api("/hooks/#{hook.id}") - - expect(response).to have_gitlab_http_status(:unauthorized) - end + def hook_uri(hook_id = hook.id) + "/hooks/#{hook_id}" end - context "when not an admin" do - it "returns forbidden error" do - get api("/hooks/#{hook.id}", user) - - expect(response).to have_gitlab_http_status(:forbidden) - end - end - - context "when authenticated as admin" do - it "gets a hook", :aggregate_failures do - get api("/hooks/#{hook.id}", admin) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/system_hook') - expect(json_response).to match( - 'id' => be(hook.id), - 'url' => eq(hook.url), - 'created_at' => eq(hook.created_at.iso8601(3)), - 'push_events' => be(hook.push_events), - 'tag_push_events' => be(hook.tag_push_events), - 'merge_requests_events' => be(hook.merge_requests_events), - 'repository_update_events' => be(hook.repository_update_events), - 'enable_ssl_verification' => be(hook.enable_ssl_verification), - 'alert_status' => eq(hook.alert_status.to_s), - 'disabled_until' => eq(hook.disabled_until&.iso8601(3)) - ) - end - - context 'the hook is disabled' do - before do - hook.disable! - end - - it "has the correct alert status", :aggregate_failures do - get api("/hooks/#{hook.id}", admin) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/system_hook') - expect(json_response).to include('alert_status' => 'disabled') - end - end - - context 'the hook is backed-off' do - before do - hook.backoff! - end - - it "has the correct alert status", :aggregate_failures do - get api("/hooks/#{hook.id}", admin) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/system_hook') - expect(json_response).to include( - 'alert_status' => 'temporarily_disabled', - 'disabled_until' => hook.disabled_until.iso8601(3) - ) - end - end - - it 'returns 404 if the system hook does not exist' do - get api("/hooks/#{non_existing_record_id}", admin) - - expect(response).to have_gitlab_http_status(:not_found) - end + def match_hook_schema + match_response_schema('public_api/v4/system_hook') end - end - describe "POST /hooks" do - it "creates new hook" do - expect do - post api("/hooks", admin), params: { url: 'http://example.com' } - end.to change { SystemHook.count }.by(1) + def event_names + %i[ + push_events + tag_push_events + merge_requests_events + repository_update_events + ] end - it "responds with 400 if url not given" do - post api("/hooks", admin) - - expect(response).to have_gitlab_http_status(:bad_request) + def hook_param_overrides + {} end - it "responds with 400 if url is invalid" do - post api("/hooks", admin), params: { url: 'hp://mep.mep' } - - expect(response).to have_gitlab_http_status(:bad_request) + let(:update_params) do + { + push_events: false, + tag_push_events: true + } end - it "does not create new hook without url" do - expect do - post api("/hooks", admin) - end.not_to change { SystemHook.count } + let(:default_values) do + { repository_update_events: true } end - it 'sets default values for events' do - stub_full_request('http://mep.mep', method: :post) - - post api('/hooks', admin), params: { url: 'http://mep.mep' } - - expect(response).to have_gitlab_http_status(:created) - expect(response).to match_response_schema('public_api/v4/system_hook') - expect(json_response['enable_ssl_verification']).to be true - expect(json_response['push_events']).to be false - expect(json_response['tag_push_events']).to be false - expect(json_response['merge_requests_events']).to be false - expect(json_response['repository_update_events']).to be true - end - - it 'sets explicit values for events' do - stub_full_request('http://mep.mep', method: :post) - - post api('/hooks', admin), - params: { - url: 'http://mep.mep', - enable_ssl_verification: false, - push_events: true, - tag_push_events: true, - merge_requests_events: true, - repository_update_events: false - } - - expect(response).to have_gitlab_http_status(:created) - expect(response).to match_response_schema('public_api/v4/system_hook') - expect(json_response['enable_ssl_verification']).to be false - expect(json_response['push_events']).to be true - expect(json_response['tag_push_events']).to be true - expect(json_response['merge_requests_events']).to be true - expect(json_response['repository_update_events']).to be false - end - end - - describe 'POST /hooks/:id' do - it "returns and trigger hook by id" do - post api("/hooks/#{hook.id}", admin) - expect(response).to have_gitlab_http_status(:created) - expect(json_response['event_name']).to eq('project_create') - end - - it "returns 404 on failure" do - post api("/hooks/404", admin) - expect(response).to have_gitlab_http_status(:not_found) - end - end - - describe "DELETE /hooks/:id" do - it "deletes a hook" do - expect do - delete api("/hooks/#{hook.id}", admin) - - expect(response).to have_gitlab_http_status(:no_content) - end.to change { SystemHook.count }.by(-1) - end - - it 'returns 404 if the system hook does not exist' do - delete api("/hooks/#{non_existing_record_id}", admin) - - expect(response).to have_gitlab_http_status(:not_found) - end - - it_behaves_like '412 response' do - let(:request) { api("/hooks/#{hook.id}", admin) } - end + it_behaves_like 'web-hook API endpoints test hook', '' end end diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index 3558babf2f1..e81e9e0bf2f 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -90,6 +90,13 @@ RSpec.describe API::Tags do let(:request) { get api(route, current_user) } end end + + context 'when repository does not exist' do + it_behaves_like '404 response' do + let(:project) { create(:project, creator: user) } + let(:request) { get api(route, current_user) } + end + end end context 'when unauthenticated', 'and project is public' do diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb index 12bce4da011..dff44a45de4 100644 --- a/spec/requests/api/terraform/modules/v1/packages_spec.rb +++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb @@ -98,6 +98,216 @@ RSpec.describe API::Terraform::Modules::V1::Packages do end end + describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/download' do + context 'empty registry' do + let(:url) { api("/packages/terraform/modules/v1/#{group.path}/module-2/system/download") } + let(:headers) { {} } + + subject { get(url, headers: headers) } + + it 'returns not found when there is no module' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with valid namespace' do + let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/download") } + let(:headers) { {} } + + subject { get(url, headers: headers) } + + before_all do + create(:terraform_module_package, project: project, name: package.name, version: '1.0.1') + end + + where(:visibility, :user_role, :member, :token_type, :shared_examples_name, :expected_status) do + :public | :developer | true | :personal_access_token | 'redirects to version download' | :found + :public | :guest | true | :personal_access_token | 'redirects to version download' | :found + :public | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :developer | false | :personal_access_token | 'redirects to version download' | :found + :public | :guest | false | :personal_access_token | 'redirects to version download' | :found + :public | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :anonymous | false | nil | 'redirects to version download' | :found + :private | :developer | true | :personal_access_token | 'redirects to version download' | :found + :private | :guest | true | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | false | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :private | :guest | false | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :anonymous | false | nil | 'rejects terraform module packages access' | :unauthorized + :public | :developer | true | :job_token | 'redirects to version download' | :found + :public | :guest | true | :job_token | 'redirects to version download' | :found + :public | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :developer | false | :job_token | 'redirects to version download' | :found + :public | :guest | false | :job_token | 'redirects to version download' | :found + :public | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | true | :job_token | 'redirects to version download' | :found + :private | :guest | true | :job_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | false | :job_token | 'rejects terraform module packages access' | :forbidden + :private | :guest | false | :job_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized + end + + with_them do + let(:headers) { user_role == :anonymous ? {} : { 'Authorization' => "Bearer #{token}" } } + + before do + group.update!(visibility: visibility.to_s) + project.update!(visibility: visibility.to_s) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + end + + describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system' do + context 'empty registry' do + let(:url) { api("/packages/terraform/modules/v1/#{group.path}/non-existent/system") } + let(:headers) { { 'Authorization' => "Bearer #{tokens[:personal_access_token]}" } } + + subject { get(url, headers: headers) } + + it 'returns not found when there is no module' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with valid namespace' do + let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}") } + + subject { get(url, headers: headers) } + + where(:visibility, :user_role, :member, :token_type, :shared_examples_name, :expected_status) do + :public | :developer | true | :personal_access_token | 'returns terraform module version' | :success + :public | :guest | true | :personal_access_token | 'returns terraform module version' | :success + :public | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :developer | false | :personal_access_token | 'returns terraform module version' | :success + :public | :guest | false | :personal_access_token | 'returns terraform module version' | :success + :public | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :anonymous | false | nil | 'returns terraform module version' | :success + :private | :developer | true | :personal_access_token | 'returns terraform module version' | :success + :private | :guest | true | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | false | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :private | :guest | false | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :anonymous | false | nil | 'rejects terraform module packages access' | :unauthorized + :public | :developer | true | :job_token | 'returns terraform module version' | :success + :public | :guest | true | :job_token | 'returns terraform module version' | :success + :public | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :developer | false | :job_token | 'returns terraform module version' | :success + :public | :guest | false | :job_token | 'returns terraform module version' | :success + :public | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | true | :job_token | 'returns terraform module version' | :success + :private | :guest | true | :job_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | false | :job_token | 'rejects terraform module packages access' | :forbidden + :private | :guest | false | :job_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized + end + + with_them do + let(:headers) { user_role == :anonymous ? {} : { 'Authorization' => "Bearer #{token}" } } + + before do + group.update!(visibility: visibility.to_s) + project.update!(visibility: visibility.to_s) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + end + + describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version' do + let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}") } + let(:headers) { {} } + + subject { get(url, headers: headers) } + + context 'not found' do + let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/2.0.0") } + let(:headers) { { 'Authorization' => "Bearer #{tokens[:job_token]}" } } + + subject { get(url, headers: headers) } + + it 'returns not found when the specified version is not present in the registry' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with valid namespace' do + where(:visibility, :user_role, :member, :token_type, :shared_examples_name, :expected_status) do + :public | :developer | true | :personal_access_token | 'returns terraform module version' | :success + :public | :guest | true | :personal_access_token | 'returns terraform module version' | :success + :public | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :developer | false | :personal_access_token | 'returns terraform module version' | :success + :public | :guest | false | :personal_access_token | 'returns terraform module version' | :success + :public | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :anonymous | false | nil | 'returns terraform module version' | :success + :private | :developer | true | :personal_access_token | 'returns terraform module version' | :success + :private | :guest | true | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | false | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :private | :guest | false | :personal_access_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :anonymous | false | nil | 'rejects terraform module packages access' | :unauthorized + :public | :developer | true | :job_token | 'returns terraform module version' | :success + :public | :guest | true | :job_token | 'returns terraform module version' | :success + :public | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :developer | false | :job_token | 'returns terraform module version' | :success + :public | :guest | false | :job_token | 'returns terraform module version' | :success + :public | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :public | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | true | :job_token | 'returns terraform module version' | :success + :private | :guest | true | :job_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :developer | false | :job_token | 'rejects terraform module packages access' | :forbidden + :private | :guest | false | :job_token | 'rejects terraform module packages access' | :forbidden + :private | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized + :private | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized + end + + with_them do + let(:headers) { user_role == :anonymous ? {} : { 'Authorization' => "Bearer #{token}" } } + + before do + group.update!(visibility: visibility.to_s) + project.update!(visibility: visibility.to_s) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + end + describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version/download' do let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}/download") } let(:headers) { {} } diff --git a/spec/requests/api/unleash_spec.rb b/spec/requests/api/unleash_spec.rb index 6cb801538c6..7bdb89fb286 100644 --- a/spec/requests/api/unleash_spec.rb +++ b/spec/requests/api/unleash_spec.rb @@ -168,7 +168,7 @@ RSpec.describe API::Unleash do end %w(/feature_flags/unleash/:project_id/features /feature_flags/unleash/:project_id/client/features).each do |features_endpoint| - describe "GET #{features_endpoint}" do + describe "GET #{features_endpoint}", :use_clean_rails_redis_caching do let(:features_url) { features_endpoint.sub(':project_id', project_id.to_s) } let(:client) { create(:operations_feature_flags_client, project: project) } @@ -176,6 +176,46 @@ RSpec.describe API::Unleash do it_behaves_like 'authenticated request' + context 'when a client fetches feature flags several times' do + let(:headers) { { 'UNLEASH-INSTANCEID' => client.token, 'UNLEASH-APPNAME' => 'production' } } + + before do + create_list(:operations_feature_flag, 3, project: project) + end + + it 'serializes feature flags for the first time and read cached data from the second time' do + expect(API::Entities::Unleash::ClientFeatureFlags) + .to receive(:represent).with(instance_of(Operations::FeatureFlagsClient), any_args) + .once + + 5.times { get api(features_url), params: params, headers: headers } + end + + it 'increments the cache key when feature flags are modified' do + expect(API::Entities::Unleash::ClientFeatureFlags) + .to receive(:represent).with(instance_of(Operations::FeatureFlagsClient), any_args) + .twice + + 2.times { get api(features_url), params: params, headers: headers } + + ::FeatureFlags::CreateService.new(project, project.owner, name: 'feature_flag').execute + + 3.times { get api(features_url), params: params, headers: headers } + end + + context 'when cache_unleash_client_api is disabled' do + before do + stub_feature_flags(cache_unleash_client_api: false) + end + + it 'serializes feature flags every time' do + expect(::API::Entities::UnleashFeature).to receive(:represent).exactly(5).times + + 5.times { get api(features_url), params: params, headers: headers } + end + end + end + context 'with version 2 feature flags' do it 'does not return a flag without any strategies' do create(:operations_feature_flag, project: project, diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index d4dc7375e9e..68d5fad8ff4 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -17,6 +17,8 @@ RSpec.describe API::Users do let(:deactivated_user) { create(:user, state: 'deactivated') } let(:banned_user) { create(:user, :banned) } let(:internal_user) { create(:user, :bot) } + let(:user_with_2fa) { create(:user, :two_factor_via_otp) } + let(:admin_with_2fa) { create(:admin, :two_factor_via_otp) } context 'admin notes' do let_it_be(:admin) { create(:admin, note: '2019-10-06 | 2FA added | user requested | www.gitlab.com') } @@ -81,6 +83,79 @@ RSpec.describe API::Users do end end + describe "PATCH /users/:id/disable_two_factor" do + context "when current user is an admin" do + it "returns a 204 when 2FA is disabled for the target user" do + expect do + patch api("/users/#{user_with_2fa.id}/disable_two_factor", admin) + end.to change { user_with_2fa.reload.two_factor_enabled? } + .from(true) + .to(false) + expect(response).to have_gitlab_http_status(:no_content) + end + + it "uses TwoFactor Destroy Service" do + destroy_service = instance_double(TwoFactor::DestroyService, execute: nil) + expect(TwoFactor::DestroyService).to receive(:new) + .with(admin, user: user_with_2fa) + .and_return(destroy_service) + expect(destroy_service).to receive(:execute) + + patch api("/users/#{user_with_2fa.id}/disable_two_factor", admin) + end + + it "returns a 400 if 2FA is not enabled for the target user" do + expect(TwoFactor::DestroyService).to receive(:new).and_call_original + + expect do + patch api("/users/#{user.id}/disable_two_factor", admin) + end.not_to change { user.reload.two_factor_enabled? } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq("400 Bad request - Two-factor authentication is not enabled for this user") + end + + it "returns a 403 if the target user is an admin" do + expect(TwoFactor::DestroyService).to receive(:new).never + + expect do + patch api("/users/#{admin_with_2fa.id}/disable_two_factor", admin) + end.not_to change { admin_with_2fa.reload.two_factor_enabled? } + + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['message']).to eq("403 Forbidden - Two-factor authentication for admins cannot be disabled via the API. Use the Rails console") + end + + it "returns a 404 if the target user cannot be found" do + expect(TwoFactor::DestroyService).to receive(:new).never + + patch api("/users/#{non_existing_record_id}/disable_two_factor", admin) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq("404 User Not Found") + end + end + + context "when current user is not an admin" do + it "returns a 403" do + expect do + patch api("/users/#{user_with_2fa.id}/disable_two_factor", user) + end.not_to change { user_with_2fa.reload.two_factor_enabled? } + + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response['message']).to eq("403 Forbidden") + end + end + + context "when unauthenticated" do + it "returns a 401" do + patch api("/users/#{user_with_2fa.id}/disable_two_factor") + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + describe 'GET /users/' do context 'when unauthenticated' do it "does not contain certain fields" do @@ -110,6 +185,40 @@ RSpec.describe API::Users do expect(json_response.first['note']).to eq '2018-11-05 | 2FA removed | user requested | www.gitlab.com' end end + + context 'N+1 queries' do + before do + create_list(:user, 2) + end + + it 'avoids N+1 queries when requested by admin' do + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + get api("/users", admin) + end.count + + create_list(:user, 3) + + # There is a still a pending N+1 query related to fetching + # project count for each user. + # Refer issue https://gitlab.com/gitlab-org/gitlab/-/issues/367080 + + expect do + get api("/users", admin) + end.not_to exceed_all_query_limit(control_count + 3) + end + + it 'avoids N+1 queries when requested by a regular user' do + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + get api("/users", user) + end.count + + create_list(:user, 3) + + expect do + get api("/users", user) + end.not_to exceed_all_query_limit(control_count) + end + end end end -- cgit v1.2.3