Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-17 19:05:49 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-17 19:05:49 +0300
commit43a25d93ebdabea52f99b05e15b06250cd8f07d7 (patch)
treedceebdc68925362117480a5d672bcff122fb625b /spec/requests
parent20c84b99005abd1c82101dfeff264ac50d2df211 (diff)
Add latest changes from gitlab-org/gitlab@16-0-stable-eev16.0.0-rc42
Diffstat (limited to 'spec/requests')
-rw-r--r--spec/requests/abuse_reports_controller_spec.rb1
-rw-r--r--spec/requests/admin/abuse_reports_controller_spec.rb92
-rw-r--r--spec/requests/admin/applications_controller_spec.rb2
-rw-r--r--spec/requests/admin/background_migrations_controller_spec.rb11
-rw-r--r--spec/requests/admin/broadcast_messages_controller_spec.rb5
-rw-r--r--spec/requests/admin/impersonation_tokens_controller_spec.rb2
-rw-r--r--spec/requests/admin/integrations_controller_spec.rb14
-rw-r--r--spec/requests/admin/projects_controller_spec.rb86
-rw-r--r--spec/requests/admin/users_controller_spec.rb42
-rw-r--r--spec/requests/admin/version_check_controller_spec.rb2
-rw-r--r--spec/requests/api/access_requests_spec.rb2
-rw-r--r--spec/requests/api/admin/batched_background_migrations_spec.rb93
-rw-r--r--spec/requests/api/admin/ci/variables_spec.rb131
-rw-r--r--spec/requests/api/admin/instance_clusters_spec.rb139
-rw-r--r--spec/requests/api/admin/plan_limits_spec.rb64
-rw-r--r--spec/requests/api/admin/sidekiq_spec.rb27
-rw-r--r--spec/requests/api/api_guard/admin_mode_middleware_spec.rb2
-rw-r--r--spec/requests/api/api_guard/response_coercer_middleware_spec.rb2
-rw-r--r--spec/requests/api/api_spec.rb24
-rw-r--r--spec/requests/api/appearance_spec.rb22
-rw-r--r--spec/requests/api/applications_spec.rb10
-rw-r--r--spec/requests/api/avatar_spec.rb1
-rw-r--r--spec/requests/api/award_emoji_spec.rb2
-rw-r--r--spec/requests/api/badges_spec.rb4
-rw-r--r--spec/requests/api/broadcast_messages_spec.rb87
-rw-r--r--spec/requests/api/bulk_imports_spec.rb87
-rw-r--r--spec/requests/api/ci/job_artifacts_spec.rb6
-rw-r--r--spec/requests/api/ci/jobs_spec.rb52
-rw-r--r--spec/requests/api/ci/pipeline_schedules_spec.rb4
-rw-r--r--spec/requests/api/ci/pipelines_spec.rb197
-rw-r--r--spec/requests/api/ci/runner/jobs_artifacts_spec.rb63
-rw-r--r--spec/requests/api/ci/runner/jobs_put_spec.rb6
-rw-r--r--spec/requests/api/ci/runner/jobs_request_post_spec.rb115
-rw-r--r--spec/requests/api/ci/runner/runners_delete_spec.rb92
-rw-r--r--spec/requests/api/ci/runner/runners_post_spec.rb81
-rw-r--r--spec/requests/api/ci/runner/runners_verify_post_spec.rb69
-rw-r--r--spec/requests/api/ci/runners_reset_registration_token_spec.rb15
-rw-r--r--spec/requests/api/ci/runners_spec.rb166
-rw-r--r--spec/requests/api/ci/secure_files_spec.rb2
-rw-r--r--spec/requests/api/ci/variables_spec.rb2
-rw-r--r--spec/requests/api/clusters/agent_tokens_spec.rb19
-rw-r--r--spec/requests/api/clusters/agents_spec.rb2
-rw-r--r--spec/requests/api/commit_statuses_spec.rb4
-rw-r--r--spec/requests/api/commits_spec.rb63
-rw-r--r--spec/requests/api/composer_packages_spec.rb6
-rw-r--r--spec/requests/api/conan_project_packages_spec.rb23
-rw-r--r--spec/requests/api/debian_group_packages_spec.rb69
-rw-r--r--spec/requests/api/debian_project_packages_spec.rb89
-rw-r--r--spec/requests/api/deploy_keys_spec.rb134
-rw-r--r--spec/requests/api/deploy_tokens_spec.rb45
-rw-r--r--spec/requests/api/deployments_spec.rb6
-rw-r--r--spec/requests/api/doorkeeper_access_spec.rb2
-rw-r--r--spec/requests/api/draft_notes_spec.rb214
-rw-r--r--spec/requests/api/environments_spec.rb40
-rw-r--r--spec/requests/api/error_tracking/project_settings_spec.rb359
-rw-r--r--spec/requests/api/files_spec.rb35
-rw-r--r--spec/requests/api/freeze_periods_spec.rb68
-rw-r--r--spec/requests/api/graphql/achievements/user_achievements_query_spec.rb80
-rw-r--r--spec/requests/api/graphql/ci/ci_cd_setting_spec.rb1
-rw-r--r--spec/requests/api/graphql/ci/config_variables_spec.rb4
-rw-r--r--spec/requests/api/graphql/ci/group_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb108
-rw-r--r--spec/requests/api/graphql/ci/instance_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/job_spec.rb3
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb186
-rw-r--r--spec/requests/api/graphql/ci/manual_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/project_variables_spec.rb2
-rw-r--r--spec/requests/api/graphql/ci/runner_spec.rb350
-rw-r--r--spec/requests/api/graphql/ci/runners_spec.rb31
-rw-r--r--spec/requests/api/graphql/current_user/todos_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/current_user_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/custom_emoji_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/data_transfer_spec.rb115
-rw-r--r--spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb10
-rw-r--r--spec/requests/api/graphql/group/labels_query_spec.rb19
-rw-r--r--spec/requests/api/graphql/group/milestones_spec.rb6
-rw-r--r--spec/requests/api/graphql/issues_spec.rb34
-rw-r--r--spec/requests/api/graphql/jobs_query_spec.rb41
-rw-r--r--spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb16
-rw-r--r--spec/requests/api/graphql/metrics/dashboard_query_spec.rb15
-rw-r--r--spec/requests/api/graphql/multiplexed_queries_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/achievements/award_spec.rb106
-rw-r--r--spec/requests/api/graphql/mutations/achievements/delete_spec.rb79
-rw-r--r--spec/requests/api/graphql/mutations/achievements/revoke_spec.rb91
-rw-r--r--spec/requests/api/graphql/mutations/achievements/update_spec.rb90
-rw-r--r--spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/add_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job/cancel_spec.rb (renamed from spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb)4
-rw-r--r--spec/requests/api/graphql/mutations/ci/job/play_spec.rb (renamed from spec/requests/api/graphql/mutations/ci/job_play_spec.rb)6
-rw-r--r--spec/requests/api/graphql/mutations/ci/job/retry_spec.rb (renamed from spec/requests/api/graphql/mutations/ci/job_retry_spec.rb)8
-rw-r--r--spec/requests/api/graphql/mutations/ci/job/unschedule_spec.rb (renamed from spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb)2
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb197
-rw-r--r--spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb19
-rw-r--r--spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb108
-rw-r--r--spec/requests/api/graphql/mutations/ci/runner/create_spec.rb313
-rw-r--r--spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb8
-rw-r--r--spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/design_management/update_spec.rb77
-rw-r--r--spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb68
-rw-r--r--spec/requests/api/graphql/mutations/issues/create_spec.rb1
-rw-r--r--spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb128
-rw-r--r--spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb18
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb15
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb15
-rw-r--r--spec/requests/api/graphql/mutations/notes/create/note_spec.rb17
-rw-r--r--spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb153
-rw-r--r--spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/releases/create_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/snippets/update_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/user_preferences/update_spec.rb12
-rw-r--r--spec/requests/api/graphql/mutations/work_items/convert_spec.rb61
-rw-r--r--spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb3
-rw-r--r--spec/requests/api/graphql/mutations/work_items/create_spec.rb152
-rw-r--r--spec/requests/api/graphql/mutations/work_items/export_spec.rb71
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_spec.rb730
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_task_spec.rb2
-rw-r--r--spec/requests/api/graphql/namespace/projects_spec.rb2
-rw-r--r--spec/requests/api/graphql/packages/package_spec.rb25
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb25
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/base_service_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb122
-rw-r--r--spec/requests/api/graphql/project/cluster_agents_spec.rb5
-rw-r--r--spec/requests/api/graphql/project/commit_references_spec.rb240
-rw-r--r--spec/requests/api/graphql/project/container_repositories_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/data_transfer_spec.rb112
-rw-r--r--spec/requests/api/graphql/project/environments_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/flow_metrics_spec.rb23
-rw-r--r--spec/requests/api/graphql/project/fork_details_spec.rb34
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb27
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb27
-rw-r--r--spec/requests/api/graphql/project/milestones_spec.rb29
-rw-r--r--spec/requests/api/graphql/project/project_statistics_redirect_spec.rb78
-rw-r--r--spec/requests/api/graphql/project/release_spec.rb8
-rw-r--r--spec/requests/api/graphql/project/user_access_authorized_agents_spec.rb129
-rw-r--r--spec/requests/api/graphql/project/work_items_spec.rb132
-rw-r--r--spec/requests/api/graphql/project_query_spec.rb61
-rw-r--r--spec/requests/api/graphql/query_spec.rb36
-rw-r--r--spec/requests/api/graphql/user/user_achievements_query_spec.rb95
-rw-r--r--spec/requests/api/graphql/user_spec.rb18
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb186
-rw-r--r--spec/requests/api/graphql_spec.rb2
-rw-r--r--spec/requests/api/group_clusters_spec.rb2
-rw-r--r--spec/requests/api/group_milestones_spec.rb78
-rw-r--r--spec/requests/api/group_variables_spec.rb2
-rw-r--r--spec/requests/api/groups_spec.rb367
-rw-r--r--spec/requests/api/helm_packages_spec.rb15
-rw-r--r--spec/requests/api/helpers_spec.rb2
-rw-r--r--spec/requests/api/import_github_spec.rb74
-rw-r--r--spec/requests/api/integrations/slack/events_spec.rb91
-rw-r--r--spec/requests/api/integrations/slack/interactions_spec.rb69
-rw-r--r--spec/requests/api/integrations/slack/options_spec.rb64
-rw-r--r--spec/requests/api/integrations_spec.rb54
-rw-r--r--spec/requests/api/internal/base_spec.rb98
-rw-r--r--spec/requests/api/internal/kubernetes_spec.rb177
-rw-r--r--spec/requests/api/internal/pages_spec.rb382
-rw-r--r--spec/requests/api/internal/workhorse_spec.rb2
-rw-r--r--spec/requests/api/issue_links_spec.rb6
-rw-r--r--spec/requests/api/issues/get_group_issues_spec.rb30
-rw-r--r--spec/requests/api/issues/get_project_issues_spec.rb62
-rw-r--r--spec/requests/api/issues/issues_spec.rb82
-rw-r--r--spec/requests/api/issues/post_projects_issues_spec.rb24
-rw-r--r--spec/requests/api/issues/put_projects_issues_spec.rb69
-rw-r--r--spec/requests/api/keys_spec.rb47
-rw-r--r--spec/requests/api/lint_spec.rb228
-rw-r--r--spec/requests/api/maven_packages_spec.rb246
-rw-r--r--spec/requests/api/members_spec.rb40
-rw-r--r--spec/requests/api/merge_requests_spec.rb149
-rw-r--r--spec/requests/api/metadata_spec.rb2
-rw-r--r--spec/requests/api/metrics/dashboard/annotations_spec.rb15
-rw-r--r--spec/requests/api/metrics/user_starred_dashboards_spec.rb28
-rw-r--r--spec/requests/api/ml/mlflow/experiments_spec.rb215
-rw-r--r--spec/requests/api/ml/mlflow/runs_spec.rb354
-rw-r--r--spec/requests/api/ml/mlflow_spec.rb630
-rw-r--r--spec/requests/api/namespaces_spec.rb74
-rw-r--r--spec/requests/api/notes_spec.rb10
-rw-r--r--spec/requests/api/npm_instance_packages_spec.rb34
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb127
-rw-r--r--spec/requests/api/nuget_group_packages_spec.rb12
-rw-r--r--spec/requests/api/nuget_project_packages_spec.rb11
-rw-r--r--spec/requests/api/oauth_tokens_spec.rb4
-rw-r--r--spec/requests/api/package_files_spec.rb84
-rw-r--r--spec/requests/api/pages/internal_access_spec.rb68
-rw-r--r--spec/requests/api/pages/pages_spec.rb22
-rw-r--r--spec/requests/api/pages/private_access_spec.rb68
-rw-r--r--spec/requests/api/pages/public_access_spec.rb68
-rw-r--r--spec/requests/api/pages_domains_spec.rb44
-rw-r--r--spec/requests/api/personal_access_tokens/self_information_spec.rb6
-rw-r--r--spec/requests/api/personal_access_tokens_spec.rb72
-rw-r--r--spec/requests/api/project_attributes.yml20
-rw-r--r--spec/requests/api/project_clusters_spec.rb2
-rw-r--r--spec/requests/api/project_export_spec.rb123
-rw-r--r--spec/requests/api/project_import_spec.rb108
-rw-r--r--spec/requests/api/project_job_token_scope_spec.rb76
-rw-r--r--spec/requests/api/project_milestones_spec.rb87
-rw-r--r--spec/requests/api/project_snapshots_spec.rb13
-rw-r--r--spec/requests/api/project_snippets_spec.rb136
-rw-r--r--spec/requests/api/project_templates_spec.rb25
-rw-r--r--spec/requests/api/projects_spec.rb1036
-rw-r--r--spec/requests/api/protected_branches_spec.rb124
-rw-r--r--spec/requests/api/protected_tags_spec.rb15
-rw-r--r--spec/requests/api/pypi_packages_spec.rb30
-rw-r--r--spec/requests/api/release/links_spec.rb28
-rw-r--r--spec/requests/api/releases_spec.rb37
-rw-r--r--spec/requests/api/repositories_spec.rb1
-rw-r--r--spec/requests/api/resource_access_tokens_spec.rb112
-rw-r--r--spec/requests/api/rubygem_packages_spec.rb30
-rw-r--r--spec/requests/api/search_spec.rb23
-rw-r--r--spec/requests/api/settings_spec.rb66
-rw-r--r--spec/requests/api/sidekiq_metrics_spec.rb17
-rw-r--r--spec/requests/api/snippets_spec.rb20
-rw-r--r--spec/requests/api/statistics_spec.rb8
-rw-r--r--spec/requests/api/tags_spec.rb2
-rw-r--r--spec/requests/api/terraform/modules/v1/packages_spec.rb7
-rw-r--r--spec/requests/api/terraform/state_spec.rb92
-rw-r--r--spec/requests/api/terraform/state_version_spec.rb10
-rw-r--r--spec/requests/api/topics_spec.rb95
-rw-r--r--spec/requests/api/unleash_spec.rb8
-rw-r--r--spec/requests/api/usage_data_non_sql_metrics_spec.rb10
-rw-r--r--spec/requests/api/usage_data_queries_spec.rb12
-rw-r--r--spec/requests/api/users_preferences_spec.rb5
-rw-r--r--spec/requests/api/users_spec.rb1223
-rw-r--r--spec/requests/api/v3/github_spec.rb70
-rw-r--r--spec/requests/dashboard_controller_spec.rb2
-rw-r--r--spec/requests/git_http_spec.rb29
-rw-r--r--spec/requests/groups/achievements_controller_spec.rb78
-rw-r--r--spec/requests/groups/email_campaigns_controller_spec.rb6
-rw-r--r--spec/requests/groups/observability_controller_spec.rb18
-rw-r--r--spec/requests/groups/settings/access_tokens_controller_spec.rb2
-rw-r--r--spec/requests/groups/settings/applications_controller_spec.rb2
-rw-r--r--spec/requests/groups/usage_quotas_controller_spec.rb2
-rw-r--r--spec/requests/ide_controller_spec.rb153
-rw-r--r--spec/requests/import/github_controller_spec.rb42
-rw-r--r--spec/requests/import/github_groups_controller_spec.rb2
-rw-r--r--spec/requests/import/gitlab_projects_controller_spec.rb14
-rw-r--r--spec/requests/jira_authorizations_spec.rb10
-rw-r--r--spec/requests/jira_connect/oauth_application_ids_controller_spec.rb6
-rw-r--r--spec/requests/jira_connect/public_keys_controller_spec.rb21
-rw-r--r--spec/requests/jira_connect/users_controller_spec.rb46
-rw-r--r--spec/requests/jwks_controller_spec.rb11
-rw-r--r--spec/requests/jwt_controller_spec.rb10
-rw-r--r--spec/requests/oauth/applications_controller_spec.rb2
-rw-r--r--spec/requests/oauth/authorizations_controller_spec.rb2
-rw-r--r--spec/requests/oauth/tokens_controller_spec.rb2
-rw-r--r--spec/requests/oauth_tokens_spec.rb2
-rw-r--r--spec/requests/openid_connect_spec.rb6
-rw-r--r--spec/requests/profiles/comment_templates_controller_spec.rb (renamed from spec/requests/profiles/saved_replies_controller_spec.rb)6
-rw-r--r--spec/requests/projects/airflow/dags_controller_spec.rb105
-rw-r--r--spec/requests/projects/aws/configuration_controller_spec.rb59
-rw-r--r--spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb2
-rw-r--r--spec/requests/projects/cluster_agents_controller_spec.rb2
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb2
-rw-r--r--spec/requests/projects/environments_controller_spec.rb4
-rw-r--r--spec/requests/projects/google_cloud/configuration_controller_spec.rb2
-rw-r--r--spec/requests/projects/google_cloud/databases_controller_spec.rb2
-rw-r--r--spec/requests/projects/google_cloud/deployments_controller_spec.rb114
-rw-r--r--spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb2
-rw-r--r--spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb2
-rw-r--r--spec/requests/projects/google_cloud/service_accounts_controller_spec.rb2
-rw-r--r--spec/requests/projects/incident_management/timeline_events_spec.rb4
-rw-r--r--spec/requests/projects/issue_links_controller_spec.rb26
-rw-r--r--spec/requests/projects/issues_controller_spec.rb39
-rw-r--r--spec/requests/projects/merge_requests_controller_spec.rb5
-rw-r--r--spec/requests/projects/merge_requests_discussions_spec.rb295
-rw-r--r--spec/requests/projects/merge_requests_spec.rb11
-rw-r--r--spec/requests/projects/metrics/dashboards/builder_spec.rb16
-rw-r--r--spec/requests/projects/metrics_dashboard_spec.rb12
-rw-r--r--spec/requests/projects/ml/candidates_controller_spec.rb53
-rw-r--r--spec/requests/projects/ml/experiments_controller_spec.rb230
-rw-r--r--spec/requests/projects/pipelines_controller_spec.rb36
-rw-r--r--spec/requests/projects/settings/access_tokens_controller_spec.rb2
-rw-r--r--spec/requests/projects/uploads_spec.rb2
-rw-r--r--spec/requests/projects/usage_quotas_spec.rb2
-rw-r--r--spec/requests/projects/wikis_controller_spec.rb72
-rw-r--r--spec/requests/projects/work_items_spec.rb178
-rw-r--r--spec/requests/rack_attack_global_spec.rb2
-rw-r--r--spec/requests/registrations_controller_spec.rb24
-rw-r--r--spec/requests/sandbox_controller_spec.rb2
-rw-r--r--spec/requests/search_controller_spec.rb10
-rw-r--r--spec/requests/self_monitoring_project_spec.rb213
-rw-r--r--spec/requests/sessions_spec.rb48
-rw-r--r--spec/requests/time_tracking/timelogs_controller_spec.rb46
-rw-r--r--spec/requests/users/pins_spec.rb67
-rw-r--r--spec/requests/users_controller_spec.rb142
-rw-r--r--spec/requests/verifies_with_email_spec.rb4
-rw-r--r--spec/requests/web_ide/remote_ide_controller_spec.rb2
296 files changed, 12939 insertions, 5513 deletions
diff --git a/spec/requests/abuse_reports_controller_spec.rb b/spec/requests/abuse_reports_controller_spec.rb
index 934f123e45b..4b81394aea3 100644
--- a/spec/requests/abuse_reports_controller_spec.rb
+++ b/spec/requests/abuse_reports_controller_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe AbuseReportsController, feature_category: :insider_threat do
attributes_for(:abuse_report) do |hash|
hash[:user_id] = user.id
hash[:category] = abuse_category
+ hash[:screenshot] = fixture_file_upload('spec/fixtures/dk.png')
end
end
diff --git a/spec/requests/admin/abuse_reports_controller_spec.rb b/spec/requests/admin/abuse_reports_controller_spec.rb
new file mode 100644
index 00000000000..0b5aaabaa61
--- /dev/null
+++ b/spec/requests/admin/abuse_reports_controller_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Admin::AbuseReportsController, type: :request, feature_category: :insider_threat do
+ include AdminModeHelper
+
+ let_it_be(:admin) { create(:admin) }
+
+ before do
+ enable_admin_mode!(admin)
+ sign_in(admin)
+ end
+
+ describe 'GET #index' do
+ let!(:open_report) { create(:abuse_report) }
+ let!(:closed_report) { create(:abuse_report, :closed) }
+
+ it 'returns open reports by default' do
+ get admin_abuse_reports_path
+
+ expect(assigns(:abuse_reports).count).to eq 1
+ expect(assigns(:abuse_reports).first.open?).to eq true
+ end
+
+ it 'returns reports by specified status' do
+ get admin_abuse_reports_path, params: { status: 'closed' }
+
+ expect(assigns(:abuse_reports).count).to eq 1
+ expect(assigns(:abuse_reports).first.closed?).to eq true
+ end
+
+ context 'when abuse_reports_list flag is disabled' do
+ before do
+ stub_feature_flags(abuse_reports_list: false)
+ end
+
+ it 'returns all reports by default' do
+ get admin_abuse_reports_path
+
+ expect(assigns(:abuse_reports).count).to eq 2
+ end
+ end
+ end
+
+ describe 'GET #show' do
+ let!(:report) { create(:abuse_report) }
+
+ it 'returns the requested report' do
+ get admin_abuse_report_path(report)
+
+ expect(assigns(:abuse_report)).to eq report
+ end
+ end
+
+ describe 'PUT #update' do
+ let(:report) { create(:abuse_report) }
+ let(:params) { { user_action: 'block_user', close: 'true', reason: 'spam', comment: 'obvious spam' } }
+ let(:expected_params) { ActionController::Parameters.new(params).permit! }
+
+ it 'invokes the Admin::AbuseReportUpdateService' do
+ expect_next_instance_of(Admin::AbuseReportUpdateService, report, admin, expected_params) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ put admin_abuse_report_path(report, params)
+ end
+ end
+
+ describe 'DELETE #destroy' do
+ let!(:report) { create(:abuse_report) }
+ let(:params) { {} }
+
+ subject { delete admin_abuse_report_path(report, params) }
+
+ it 'destroys the report' do
+ expect { subject }.to change { AbuseReport.count }.by(-1)
+ end
+
+ context 'when passing the `remove_user` parameter' do
+ let(:params) { { remove_user: true } }
+
+ it 'calls the `remove_user` method' do
+ expect_next_found_instance_of(AbuseReport) do |report|
+ expect(report).to receive(:remove_user).with(deleted_by: admin)
+ end
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/requests/admin/applications_controller_spec.rb b/spec/requests/admin/applications_controller_spec.rb
index c83137ebbce..367697b1289 100644
--- a/spec/requests/admin/applications_controller_spec.rb
+++ b/spec/requests/admin/applications_controller_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Admin::ApplicationsController, :enable_admin_mode,
-feature_category: :authentication_and_authorization do
+feature_category: :system_access do
let_it_be(:admin) { create(:admin) }
let_it_be(:application) { create(:oauth_application, owner_id: nil, owner_type: nil) }
let_it_be(:show_path) { admin_application_path(application) }
diff --git a/spec/requests/admin/background_migrations_controller_spec.rb b/spec/requests/admin/background_migrations_controller_spec.rb
index 88d81766e67..2681ece7d8a 100644
--- a/spec/requests/admin/background_migrations_controller_spec.rb
+++ b/spec/requests/admin/background_migrations_controller_spec.rb
@@ -67,6 +67,17 @@ RSpec.describe Admin::BackgroundMigrationsController, :enable_admin_mode, featur
expect(assigns(:migrations)).to match_array([main_database_migration])
end
+
+ context 'for finalizing tab' do
+ let!(:finalizing_migration) { create(:batched_background_migration, :finalizing) }
+
+ it 'returns only finalizing migration' do
+ get admin_background_migrations_path(tab: 'finalizing')
+
+ expect(Gitlab::Database::BackgroundMigration::BatchedMigration.queued).not_to be_empty
+ expect(assigns(:migrations)).to match_array(Array.wrap(finalizing_migration))
+ end
+ end
end
context 'when multiple database is enabled', :add_ci_connection do
diff --git a/spec/requests/admin/broadcast_messages_controller_spec.rb b/spec/requests/admin/broadcast_messages_controller_spec.rb
index 69b84d6d795..0143c9ce030 100644
--- a/spec/requests/admin/broadcast_messages_controller_spec.rb
+++ b/spec/requests/admin/broadcast_messages_controller_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe Admin::BroadcastMessagesController, :enable_admin_mode, feature_c
let_it_be(:invalid_broadcast_message) { { broadcast_message: { message: '' } } }
let_it_be(:test_message) { 'you owe me a new acorn' }
+ let_it_be(:test_preview) { '<p>Hello, world!</p>' }
before do
sign_in(create(:admin))
@@ -23,11 +24,11 @@ RSpec.describe Admin::BroadcastMessagesController, :enable_admin_mode, feature_c
end
describe 'POST /preview' do
- it 'renders preview partial' do
+ it 'renders preview html' do
post preview_admin_broadcast_messages_path, params: { broadcast_message: { message: "Hello, world!" } }
expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to render_template(:_preview)
+ expect(response.body).to eq(test_preview)
end
end
diff --git a/spec/requests/admin/impersonation_tokens_controller_spec.rb b/spec/requests/admin/impersonation_tokens_controller_spec.rb
index 15212db0e77..11fc5d94292 100644
--- a/spec/requests/admin/impersonation_tokens_controller_spec.rb
+++ b/spec/requests/admin/impersonation_tokens_controller_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Admin::ImpersonationTokensController, :enable_admin_mode,
-feature_category: :authentication_and_authorization do
+feature_category: :system_access do
let(:admin) { create(:admin) }
let!(:user) { create(:user) }
diff --git a/spec/requests/admin/integrations_controller_spec.rb b/spec/requests/admin/integrations_controller_spec.rb
index efd0e3d91ee..6240c2406ea 100644
--- a/spec/requests/admin/integrations_controller_spec.rb
+++ b/spec/requests/admin/integrations_controller_spec.rb
@@ -9,6 +9,20 @@ RSpec.describe Admin::IntegrationsController, :enable_admin_mode, feature_catego
sign_in(admin)
end
+ describe 'GET #edit' do
+ context 'when remove_monitor_metrics is true' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'renders a 404 for the prometheus integration' do
+ get edit_admin_application_settings_integration_path(:prometheus)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
describe 'GET #overrides' do
let_it_be(:integration) { create(:jira_integration, :instance) }
let_it_be(:overridden_integration) { create(:jira_integration) }
diff --git a/spec/requests/admin/projects_controller_spec.rb b/spec/requests/admin/projects_controller_spec.rb
new file mode 100644
index 00000000000..2462152b7c2
--- /dev/null
+++ b/spec/requests/admin/projects_controller_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Admin::ProjectsController, :enable_admin_mode, feature_category: :projects do
+ let_it_be(:project) { create(:project, :public, name: 'test', description: 'test') }
+ let_it_be(:admin) { create(:admin) }
+
+ describe 'PUT #update' do
+ let(:project_params) { {} }
+ let(:params) { { project: project_params } }
+ let(:path_params) { { namespace_id: project.namespace.to_param, id: project.to_param } }
+
+ before do
+ sign_in(admin)
+ end
+
+ subject do
+ put admin_namespace_project_path(path_params), params: params
+ end
+
+ context 'when changing the name' do
+ let(:project_params) { { name: 'new name' } }
+
+ it 'returns success' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:found)
+ end
+
+ it 'changes the name' do
+ expect { subject }.to change { project.reload.name }.to('new name')
+ end
+ end
+
+ context 'when changing the description' do
+ let(:project_params) { { description: 'new description' } }
+
+ it 'returns success' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:found)
+ end
+
+ it 'changes the project description' do
+ expect { subject }.to change { project.reload.description }.to('new description')
+ end
+ end
+
+ context 'when changing the name to an invalid name' do
+ let(:project_params) { { name: 'invalid/project/name' } }
+
+ it 'does not change the name' do
+ expect { subject }.not_to change { project.reload.name }
+ end
+ end
+
+ context 'when disabling runner registration' do
+ let(:project_params) { { runner_registration_enabled: false } }
+
+ it 'changes runner registration' do
+ expect { subject }.to change { project.reload.runner_registration_enabled }.to(false)
+ end
+
+ it 'resets the registration token' do
+ expect { subject }.to change { project.reload.runners_token }
+ end
+ end
+
+ context 'when enabling runner registration' do
+ before do
+ project.update!(runner_registration_enabled: false)
+ end
+
+ let(:project_params) { { runner_registration_enabled: true } }
+
+ it 'changes runner registration' do
+ expect { subject }.to change { project.reload.runner_registration_enabled }.to(true)
+ end
+
+ it 'does not reset the registration token' do
+ expect { subject }.not_to change { project.reload.runners_token }
+ end
+ end
+ end
+end
diff --git a/spec/requests/admin/users_controller_spec.rb b/spec/requests/admin/users_controller_spec.rb
new file mode 100644
index 00000000000..5344a2c2bb7
--- /dev/null
+++ b/spec/requests/admin/users_controller_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Admin::UsersController, :enable_admin_mode, feature_category: :user_management do
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:user) { create(:user) }
+
+ describe 'PUT #block' do
+ context 'when request format is :json' do
+ before do
+ sign_in(admin)
+ end
+
+ subject(:request) { put block_admin_user_path(user, format: :json) }
+
+ context 'when user was blocked' do
+ it 'returns 200 and json data with notice' do
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include('notice' => 'Successfully blocked')
+ end
+ end
+
+ context 'when user was not blocked' do
+ before do
+ allow_next_instance_of(::Users::BlockService) do |service|
+ allow(service).to receive(:execute).and_return({ status: :failed })
+ end
+ end
+
+ it 'returns 200 and json data with error' do
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include('error' => 'Error occurred. User was not blocked')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/admin/version_check_controller_spec.rb b/spec/requests/admin/version_check_controller_spec.rb
index 47221bf37e5..a998c2f426b 100644
--- a/spec/requests/admin/version_check_controller_spec.rb
+++ b/spec/requests/admin/version_check_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Admin::VersionCheckController, :enable_admin_mode, feature_category: :not_owned do
+RSpec.describe Admin::VersionCheckController, :enable_admin_mode, feature_category: :shared do
let(:admin) { create(:admin) }
before do
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 8c14ead9e42..45d1594c734 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::AccessRequests, feature_category: :authentication_and_authorization do
+RSpec.describe API::AccessRequests, feature_category: :system_access do
let_it_be(:maintainer) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:access_requester) { create(:user) }
diff --git a/spec/requests/api/admin/batched_background_migrations_spec.rb b/spec/requests/api/admin/batched_background_migrations_spec.rb
index d946ac17f3f..e88fba3fbe7 100644
--- a/spec/requests/api/admin/batched_background_migrations_spec.rb
+++ b/spec/requests/api/admin/batched_background_migrations_spec.rb
@@ -4,22 +4,23 @@ require 'spec_helper'
RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :database do
let(:admin) { create(:admin) }
- let(:unauthorized_user) { create(:user) }
describe 'GET /admin/batched_background_migrations/:id' do
let!(:migration) { create(:batched_background_migration, :paused) }
let(:database) { :main }
let(:params) { { database: database } }
+ let(:path) { "/admin/batched_background_migrations/#{migration.id}" }
+
+ it_behaves_like "GET request permissions for admin mode"
subject(:show_migration) do
- get api("/admin/batched_background_migrations/#{migration.id}", admin), params: { database: database }
+ get api(path, admin, admin_mode: true), params: { database: database }
end
it 'fetches the batched background migration' do
show_migration
aggregate_failures "testing response" do
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(migration.id)
expect(json_response['status']).to eq('paused')
expect(json_response['job_class_name']).to eq(migration.job_class_name)
@@ -29,7 +30,8 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
context 'when the batched background migration does not exist' do
it 'returns 404' do
- get api("/admin/batched_background_migrations/#{non_existing_record_id}", admin), params: params
+ get api("/admin/batched_background_migrations/#{non_existing_record_id}", admin, admin_mode: true),
+ params: params
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -50,19 +52,11 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
end
end
- context 'when authenticated as a non-admin user' do
- it 'returns 403' do
- get api("/admin/batched_background_migrations/#{migration.id}", unauthorized_user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
context 'when the database name does not exist' do
let(:database) { :wrong_database }
- it 'returns bad request' do
- get api("/admin/batched_background_migrations/#{migration.id}", admin), params: params
+ it 'returns bad request', :aggregate_failures do
+ get api(path, admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to include('database does not have a valid value')
@@ -72,13 +66,15 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
describe 'GET /admin/batched_background_migrations' do
let!(:migration) { create(:batched_background_migration) }
+ let(:path) { '/admin/batched_background_migrations' }
+
+ it_behaves_like "GET request permissions for admin mode"
context 'when is an admin user' do
it 'returns batched background migrations' do
- get api('/admin/batched_background_migrations', admin)
+ get api(path, admin, admin_mode: true)
aggregate_failures "testing response" do
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response.count).to eq(1)
expect(json_response.first['id']).to eq(migration.id)
expect(json_response.first['job_class_name']).to eq(migration.job_class_name)
@@ -105,14 +101,14 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield
- get api('/admin/batched_background_migrations', admin), params: params
+ get api(path, admin, admin_mode: true), params: params
end
context 'when the database name does not exist' do
let(:database) { :wrong_database }
- it 'returns bad request' do
- get api("/admin/batched_background_migrations", admin), params: params
+ it 'returns bad request', :aggregate_failures do
+ get api(path, admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to include('database does not have a valid value')
@@ -127,10 +123,9 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
create(:batched_background_migration, :active, gitlab_schema: schema)
end
- get api('/admin/batched_background_migrations', admin), params: params
+ get api(path, admin, admin_mode: true), params: params
aggregate_failures "testing response" do
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response.count).to eq(1)
expect(json_response.first['id']).to eq(ci_database_migration.id)
expect(json_response.first['job_class_name']).to eq(ci_database_migration.job_class_name)
@@ -142,30 +137,24 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
end
end
end
-
- context 'when authenticated as a non-admin user' do
- it 'returns 403' do
- get api('/admin/batched_background_migrations', unauthorized_user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
end
describe 'PUT /admin/batched_background_migrations/:id/resume' do
let!(:migration) { create(:batched_background_migration, :paused) }
let(:database) { :main }
let(:params) { { database: database } }
+ let(:path) { "/admin/batched_background_migrations/#{migration.id}/resume" }
+
+ it_behaves_like "PUT request permissions for admin mode"
subject(:resume) do
- put api("/admin/batched_background_migrations/#{migration.id}/resume", admin), params: params
+ put api(path, admin, admin_mode: true), params: params
end
it 'pauses the batched background migration' do
resume
aggregate_failures "testing response" do
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(migration.id)
expect(json_response['status']).to eq('active')
end
@@ -173,7 +162,8 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
context 'when the batched background migration does not exist' do
it 'returns 404' do
- put api("/admin/batched_background_migrations/#{non_existing_record_id}/resume", admin), params: params
+ put api("/admin/batched_background_migrations/#{non_existing_record_id}/resume", admin, admin_mode: true),
+ params: params
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -183,7 +173,7 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
let!(:migration) { create(:batched_background_migration, :failed) }
it 'returns 422' do
- put api("/admin/batched_background_migrations/#{migration.id}/resume", admin), params: params
+ put api(path, admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
@@ -206,34 +196,28 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
context 'when the database name does not exist' do
let(:database) { :wrong_database }
- it 'returns bad request' do
- put api("/admin/batched_background_migrations/#{migration.id}/resume", admin), params: params
+ it 'returns bad request', :aggregate_failures do
+ put api(path, admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to include('database does not have a valid value')
end
end
end
-
- context 'when authenticated as a non-admin user' do
- it 'returns 403' do
- put api("/admin/batched_background_migrations/#{migration.id}/resume", unauthorized_user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
end
describe 'PUT /admin/batched_background_migrations/:id/pause' do
let!(:migration) { create(:batched_background_migration, :active) }
let(:database) { :main }
let(:params) { { database: database } }
+ let(:path) { "/admin/batched_background_migrations/#{migration.id}/pause" }
+
+ it_behaves_like "PUT request permissions for admin mode"
it 'pauses the batched background migration' do
- put api("/admin/batched_background_migrations/#{migration.id}/pause", admin), params: params
+ put api(path, admin, admin_mode: true), params: params
aggregate_failures "testing response" do
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(migration.id)
expect(json_response['status']).to eq('paused')
end
@@ -241,7 +225,8 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
context 'when the batched background migration does not exist' do
it 'returns 404' do
- put api("/admin/batched_background_migrations/#{non_existing_record_id}/pause", admin), params: params
+ put api("/admin/batched_background_migrations/#{non_existing_record_id}/pause", admin, admin_mode: true),
+ params: params
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -251,7 +236,7 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
let!(:migration) { create(:batched_background_migration, :failed) }
it 'returns 422' do
- put api("/admin/batched_background_migrations/#{migration.id}/pause", admin), params: params
+ put api(path, admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
@@ -268,27 +253,19 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations, feature_category: :datab
it 'uses the correct connection' do
expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield
- put api("/admin/batched_background_migrations/#{migration.id}/pause", admin), params: params
+ put api(path, admin, admin_mode: true), params: params
end
context 'when the database name does not exist' do
let(:database) { :wrong_database }
- it 'returns bad request' do
- put api("/admin/batched_background_migrations/#{migration.id}/pause", admin), params: params
+ it 'returns bad request', :aggregate_failures do
+ put api(path, admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(response.body).to include('database does not have a valid value')
end
end
end
-
- context 'when authenticated as a non-admin user' do
- it 'returns 403' do
- put api("/admin/batched_background_migrations/#{non_existing_record_id}/pause", unauthorized_user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
end
end
diff --git a/spec/requests/api/admin/ci/variables_spec.rb b/spec/requests/api/admin/ci/variables_spec.rb
index 4bdc44cb583..cd57cde74ff 100644
--- a/spec/requests/api/admin/ci/variables_spec.rb
+++ b/spec/requests/api/admin/ci/variables_spec.rb
@@ -2,71 +2,63 @@
require 'spec_helper'
-RSpec.describe ::API::Admin::Ci::Variables do
+RSpec.describe ::API::Admin::Ci::Variables, :aggregate_failures, feature_category: :pipeline_composition do
let_it_be(:admin) { create(:admin) }
let_it_be(:user) { create(:user) }
+ let_it_be(:variable) { create(:ci_instance_variable) }
+ let_it_be(:path) { '/admin/ci/variables' }
describe 'GET /admin/ci/variables' do
- let!(:variable) { create(:ci_instance_variable) }
+ it_behaves_like 'GET request permissions for admin mode'
- it 'returns instance-level variables for admins', :aggregate_failures do
- get api('/admin/ci/variables', admin)
+ it 'returns instance-level variables for admins' do
+ get api(path, admin, admin_mode: true)
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_a(Array)
end
- it 'does not return instance-level variables for regular users' do
- get api('/admin/ci/variables', user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
-
it 'does not return instance-level variables for unauthorized users' do
- get api('/admin/ci/variables')
+ get api(path, admin_mode: true)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
describe 'GET /admin/ci/variables/:key' do
- let!(:variable) { create(:ci_instance_variable) }
+ let_it_be(:path) { "/admin/ci/variables/#{variable.key}" }
+
+ it_behaves_like 'GET request permissions for admin mode'
- it 'returns instance-level variable details for admins', :aggregate_failures do
- get api("/admin/ci/variables/#{variable.key}", admin)
+ it 'returns instance-level variable details for admins' do
+ get api(path, admin, admin_mode: true)
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response['value']).to eq(variable.value)
expect(json_response['protected']).to eq(variable.protected?)
expect(json_response['variable_type']).to eq(variable.variable_type)
end
it 'responds with 404 Not Found if requesting non-existing variable' do
- get api('/admin/ci/variables/non_existing_variable', admin)
+ get api('/admin/ci/variables/non_existing_variable', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
- it 'does not return instance-level variable details for regular users' do
- get api("/admin/ci/variables/#{variable.key}", user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
-
it 'does not return instance-level variable details for unauthorized users' do
- get api("/admin/ci/variables/#{variable.key}")
+ get api(path, admin_mode: true)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
describe 'POST /admin/ci/variables' do
- context 'authorized user with proper permissions' do
- let!(:variable) { create(:ci_instance_variable) }
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { { key: 'KEY', value: 'VALUE' } }
+ end
- it 'creates variable for admins', :aggregate_failures do
+ context 'authorized user with proper permissions' do
+ it 'creates variable for admins' do
expect do
- post api('/admin/ci/variables', admin),
+ post api(path, admin, admin_mode: true),
params: {
key: 'TEST_VARIABLE_2',
value: 'PROTECTED_VALUE_2',
@@ -76,7 +68,6 @@ RSpec.describe ::API::Admin::Ci::Variables do
}
end.to change { ::Ci::InstanceVariable.count }.by(1)
- expect(response).to have_gitlab_http_status(:created)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('PROTECTED_VALUE_2')
expect(json_response['protected']).to be_truthy
@@ -90,13 +81,13 @@ RSpec.describe ::API::Admin::Ci::Variables do
expect(::API::API::LOGGER).to receive(:info).with(include(params: include(masked_params)))
- post api("/admin/ci/variables", user),
+ post api(path, user, admin_mode: true),
params: { key: 'VAR_KEY', value: 'SENSITIVE', protected: true, masked: true }
end
- it 'creates variable with optional attributes', :aggregate_failures do
+ it 'creates variable with optional attributes' do
expect do
- post api('/admin/ci/variables', admin),
+ post api(path, admin, admin_mode: true),
params: {
variable_type: 'file',
key: 'TEST_VARIABLE_2',
@@ -104,7 +95,6 @@ RSpec.describe ::API::Admin::Ci::Variables do
}
end.to change { ::Ci::InstanceVariable.count }.by(1)
- expect(response).to have_gitlab_http_status(:created)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('VALUE_2')
expect(json_response['protected']).to be_falsey
@@ -115,7 +105,7 @@ RSpec.describe ::API::Admin::Ci::Variables do
it 'does not allow to duplicate variable key' do
expect do
- post api('/admin/ci/variables', admin),
+ post api(path, admin, admin_mode: true),
params: { key: variable.key, value: 'VALUE_2' }
end.not_to change { ::Ci::InstanceVariable.count }
@@ -128,7 +118,7 @@ RSpec.describe ::API::Admin::Ci::Variables do
MESSAGE
expect do
- post api('/admin/ci/variables', admin),
+ post api(path, admin, admin_mode: true),
params: { key: 'too_long', value: SecureRandom.hex(10_001) }
end.not_to change { ::Ci::InstanceVariable.count }
@@ -138,17 +128,9 @@ RSpec.describe ::API::Admin::Ci::Variables do
end
end
- context 'authorized user with invalid permissions' do
- it 'does not create variable' do
- post api('/admin/ci/variables', user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
context 'unauthorized user' do
it 'does not create variable' do
- post api('/admin/ci/variables')
+ post api(path, admin_mode: true)
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -156,20 +138,23 @@ RSpec.describe ::API::Admin::Ci::Variables do
end
describe 'PUT /admin/ci/variables/:key' do
- let!(:variable) { create(:ci_instance_variable) }
+ let_it_be(:path) { "/admin/ci/variables/#{variable.key}" }
+ let_it_be(:params) do
+ {
+ variable_type: 'file',
+ value: 'VALUE_1_UP',
+ protected: true,
+ masked: true,
+ raw: true
+ }
+ end
+
+ it_behaves_like 'PUT request permissions for admin mode'
context 'authorized user with proper permissions' do
- it 'updates variable data', :aggregate_failures do
- put api("/admin/ci/variables/#{variable.key}", admin),
- params: {
- variable_type: 'file',
- value: 'VALUE_1_UP',
- protected: true,
- masked: true,
- raw: true
- }
-
- expect(response).to have_gitlab_http_status(:ok)
+ it 'updates variable data' do
+ put api(path, admin, admin_mode: true), params: params
+
expect(variable.reload.value).to eq('VALUE_1_UP')
expect(variable.reload).to be_protected
expect(json_response['variable_type']).to eq('file')
@@ -182,28 +167,20 @@ RSpec.describe ::API::Admin::Ci::Variables do
expect(::API::API::LOGGER).to receive(:info).with(include(params: include(masked_params)))
- put api("/admin/ci/variables/#{variable.key}", admin),
+ put api(path, admin, admin_mode: true),
params: { value: 'SENSITIVE', protected: true, masked: true }
end
it 'responds with 404 Not Found if requesting non-existing variable' do
- put api('/admin/ci/variables/non_existing_variable', admin)
+ put api('/admin/ci/variables/non_existing_variable', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
end
- context 'authorized user with invalid permissions' do
- it 'does not update variable' do
- put api("/admin/ci/variables/#{variable.key}", user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
context 'unauthorized user' do
it 'does not update variable' do
- put api("/admin/ci/variables/#{variable.key}")
+ put api(path, admin_mode: true)
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -211,35 +188,27 @@ RSpec.describe ::API::Admin::Ci::Variables do
end
describe 'DELETE /admin/ci/variables/:key' do
- let!(:variable) { create(:ci_instance_variable) }
+ let_it_be(:path) { "/admin/ci/variables/#{variable.key}" }
+
+ it_behaves_like 'DELETE request permissions for admin mode'
context 'authorized user with proper permissions' do
it 'deletes variable' do
expect do
- delete api("/admin/ci/variables/#{variable.key}", admin)
-
- expect(response).to have_gitlab_http_status(:no_content)
+ delete api(path, admin, admin_mode: true)
end.to change { ::Ci::InstanceVariable.count }.by(-1)
end
it 'responds with 404 Not Found if requesting non-existing variable' do
- delete api('/admin/ci/variables/non_existing_variable', admin)
+ delete api('/admin/ci/variables/non_existing_variable', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
end
- context 'authorized user with invalid permissions' do
- it 'does not delete variable' do
- delete api("/admin/ci/variables/#{variable.key}", user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
context 'unauthorized user' do
it 'does not delete variable' do
- delete api("/admin/ci/variables/#{variable.key}")
+ delete api(path, admin_mode: true)
expect(response).to have_gitlab_http_status(:unauthorized)
end
diff --git a/spec/requests/api/admin/instance_clusters_spec.rb b/spec/requests/api/admin/instance_clusters_spec.rb
index 7b510f74fd4..f2e62533b78 100644
--- a/spec/requests/api/admin/instance_clusters_spec.rb
+++ b/spec/requests/api/admin/instance_clusters_spec.rb
@@ -2,10 +2,9 @@
require 'spec_helper'
-RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_management do
+RSpec.describe ::API::Admin::InstanceClusters, feature_category: :deployment_management do
include KubernetesHelpers
- let_it_be(:regular_user) { create(:user) }
let_it_be(:admin_user) { create(:admin) }
let_it_be(:project) { create(:project) }
let_it_be(:project_cluster) do
@@ -17,35 +16,27 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
let(:project_cluster_id) { project_cluster.id }
describe "GET /admin/clusters" do
+ let_it_be(:path) { "/admin/clusters" }
let_it_be(:clusters) do
create_list(:cluster, 3, :provided_by_gcp, :instance, :production_environment)
end
- include_examples ':certificate_based_clusters feature flag API responses' do
- let(:subject) { get api("/admin/clusters", admin_user) }
- end
+ it_behaves_like 'GET request permissions for admin mode'
- context "when authenticated as a non-admin user" do
- it 'returns 403' do
- get api('/admin/clusters', regular_user)
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ include_examples ':certificate_based_clusters feature flag API responses' do
+ let(:subject) { get api(path, admin_user, admin_mode: true) }
end
context "when authenticated as admin" do
before do
- get api("/admin/clusters", admin_user)
- end
-
- it 'returns 200' do
- expect(response).to have_gitlab_http_status(:ok)
+ get api(path, admin_user, admin_mode: true)
end
it 'includes pagination headers' do
expect(response).to include_pagination_headers
end
- it 'only returns the instance clusters' do
+ it 'only returns the instance clusters', :aggregate_failures do
cluster_ids = json_response.map { |cluster| cluster['id'] }
expect(cluster_ids).to match_array(clusters.pluck(:id))
expect(cluster_ids).not_to include(project_cluster_id)
@@ -60,19 +51,23 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
let_it_be(:cluster) do
create(:cluster, :instance, :provided_by_gcp, :with_domain,
- platform_kubernetes: platform_kubernetes,
- user: admin_user)
+ { platform_kubernetes: platform_kubernetes,
+ user: admin_user })
end
let(:cluster_id) { cluster.id }
+ let(:path) { "/admin/clusters/#{cluster_id}" }
+
+ it_behaves_like 'GET request permissions for admin mode'
+
include_examples ':certificate_based_clusters feature flag API responses' do
- let(:subject) { get api("/admin/clusters/#{cluster_id}", admin_user) }
+ let(:subject) { get api(path, admin_user, admin_mode: true) }
end
context "when authenticated as admin" do
before do
- get api("/admin/clusters/#{cluster_id}", admin_user)
+ get api(path, admin_user, admin_mode: true)
end
context "when no cluster associated to the ID" do
@@ -84,15 +79,11 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
end
context "when cluster with cluster_id exists" do
- it 'returns 200' do
- expect(response).to have_gitlab_http_status(:ok)
- end
-
it 'returns the cluster with cluster_id' do
expect(json_response['id']).to eq(cluster.id)
end
- it 'returns the cluster information' do
+ it 'returns the cluster information', :aggregate_failures do
expect(json_response['provider_type']).to eq('gcp')
expect(json_response['platform_type']).to eq('kubernetes')
expect(json_response['environment_scope']).to eq('*')
@@ -102,21 +93,21 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
expect(json_response['managed']).to be_truthy
end
- it 'returns kubernetes platform information' do
+ it 'returns kubernetes platform information', :aggregate_failures do
platform = json_response['platform_kubernetes']
expect(platform['api_url']).to eq('https://kubernetes.example.com')
expect(platform['ca_cert']).to be_present
end
- it 'returns user information' do
+ it 'returns user information', :aggregate_failures do
user = json_response['user']
expect(user['id']).to eq(admin_user.id)
expect(user['username']).to eq(admin_user.username)
end
- it 'returns GCP provider information' do
+ it 'returns GCP provider information', :aggregate_failures do
gcp_provider = json_response['provider_gcp']
expect(gcp_provider['cluster_id']).to eq(cluster.id)
@@ -140,18 +131,11 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
context 'when trying to get a project cluster via the instance cluster endpoint' do
it 'returns 404' do
- get api("/admin/clusters/#{project_cluster_id}", admin_user)
+ get api("/admin/clusters/#{project_cluster_id}", admin_user, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
-
- context "when authenticated as a non-admin user" do
- it 'returns 403' do
- get api("/admin/clusters/#{cluster_id}", regular_user)
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
end
end
@@ -159,6 +143,7 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
let(:api_url) { 'https://example.com' }
let(:authorization_type) { 'rbac' }
let(:clusterable) { Clusters::Instance.new }
+ let_it_be(:path) { '/admin/clusters/add' }
let(:platform_kubernetes_attributes) do
{
@@ -196,20 +181,20 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
}
end
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { cluster_params }
+ end
+
include_examples ':certificate_based_clusters feature flag API responses' do
- let(:subject) { post api('/admin/clusters/add', admin_user), params: cluster_params }
+ let(:subject) { post api(path, admin_user, admin_mode: true), params: cluster_params }
end
context 'authorized user' do
before do
- post api('/admin/clusters/add', admin_user), params: cluster_params
+ post api(path, admin_user, admin_mode: true), params: cluster_params
end
context 'with valid params' do
- it 'responds with 201' do
- expect(response).to have_gitlab_http_status(:created)
- end
-
it 'creates a new Clusters::Cluster', :aggregate_failures do
cluster_result = Clusters::Cluster.find(json_response["id"])
platform_kubernetes = cluster_result.platform
@@ -271,7 +256,7 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
context 'when an instance cluster already exists' do
it 'allows user to add multiple clusters' do
- post api('/admin/clusters/add', admin_user), params: multiple_cluster_params
+ post api(path, admin_user, admin_mode: true), params: multiple_cluster_params
expect(Clusters::Instance.new.clusters.count).to eq(2)
end
@@ -280,8 +265,8 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
context 'with invalid params' do
context 'when missing a required parameter' do
- it 'responds with 400' do
- post api('/admin/clusters/add', admin_user), params: invalid_cluster_params
+ it 'responds with 400', :aggregate_failures do
+ post api(path, admin_user, admin_mode: true), params: invalid_cluster_params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eql('name is missing')
end
@@ -300,14 +285,6 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
end
end
end
-
- context 'non-authorized user' do
- it 'responds with 403' do
- post api('/admin/clusters/add', regular_user), params: cluster_params
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
end
describe 'PUT /admin/clusters/:cluster_id' do
@@ -329,23 +306,25 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
create(:cluster, :instance, :provided_by_gcp, domain: 'old-domain.com')
end
+ let(:path) { "/admin/clusters/#{cluster.id}" }
+
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:params) { update_params }
+ end
+
include_examples ':certificate_based_clusters feature flag API responses' do
- let(:subject) { put api("/admin/clusters/#{cluster.id}", admin_user), params: update_params }
+ let(:subject) { put api(path, admin_user, admin_mode: true), params: update_params }
end
context 'authorized user' do
before do
- put api("/admin/clusters/#{cluster.id}", admin_user), params: update_params
+ put api(path, admin_user, admin_mode: true), params: update_params
cluster.reload
end
context 'with valid params' do
- it 'responds with 200' do
- expect(response).to have_gitlab_http_status(:ok)
- end
-
- it 'updates cluster attributes' do
+ it 'updates cluster attributes', :aggregate_failures do
expect(cluster.domain).to eq('new-domain.com')
expect(cluster.managed).to be_falsy
expect(cluster.enabled).to be_falsy
@@ -359,7 +338,7 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'does not update cluster attributes' do
+ it 'does not update cluster attributes', :aggregate_failures do
expect(cluster.domain).to eq('old-domain.com')
expect(cluster.managed).to be_truthy
expect(cluster.enabled).to be_truthy
@@ -422,7 +401,7 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
expect(response).to have_gitlab_http_status(:ok)
end
- it 'updates platform kubernetes attributes' do
+ it 'updates platform kubernetes attributes', :aggregate_failures do
platform_kubernetes = cluster.platform_kubernetes
expect(cluster.name).to eq('new-name')
@@ -435,26 +414,18 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
let(:cluster_id) { 1337 }
it 'returns 404' do
- put api("/admin/clusters/#{cluster_id}", admin_user), params: update_params
+ put api("/admin/clusters/#{cluster_id}", admin_user, admin_mode: true), params: update_params
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when trying to update a project cluster via the instance cluster endpoint' do
it 'returns 404' do
- put api("/admin/clusters/#{project_cluster_id}", admin_user), params: update_params
+ put api("/admin/clusters/#{project_cluster_id}", admin_user, admin_mode: true), params: update_params
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
-
- context 'non-authorized user' do
- it 'responds with 403' do
- put api("/admin/clusters/#{cluster.id}", regular_user), params: update_params
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
end
describe 'DELETE /admin/clusters/:cluster_id' do
@@ -464,17 +435,17 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
create(:cluster, :instance, :provided_by_gcp)
end
+ let_it_be(:path) { "/admin/clusters/#{cluster.id}" }
+
+ it_behaves_like 'DELETE request permissions for admin mode'
+
include_examples ':certificate_based_clusters feature flag API responses' do
- let(:subject) { delete api("/admin/clusters/#{cluster.id}", admin_user), params: cluster_params }
+ let(:subject) { delete api(path, admin_user, admin_mode: true), params: cluster_params }
end
context 'authorized user' do
before do
- delete api("/admin/clusters/#{cluster.id}", admin_user), params: cluster_params
- end
-
- it 'responds with 204' do
- expect(response).to have_gitlab_http_status(:no_content)
+ delete api(path, admin_user, admin_mode: true), params: cluster_params
end
it 'deletes the cluster' do
@@ -485,25 +456,17 @@ RSpec.describe ::API::Admin::InstanceClusters, feature_category: :kubernetes_man
let(:cluster_id) { 1337 }
it 'returns 404' do
- delete api("/admin/clusters/#{cluster_id}", admin_user)
+ delete api(path, admin_user, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when trying to update a project cluster via the instance cluster endpoint' do
it 'returns 404' do
- delete api("/admin/clusters/#{project_cluster_id}", admin_user)
+ delete api("/admin/clusters/#{project_cluster_id}", admin_user, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
-
- context 'non-authorized user' do
- it 'responds with 403' do
- delete api("/admin/clusters/#{cluster.id}", regular_user), params: cluster_params
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
end
end
diff --git a/spec/requests/api/admin/plan_limits_spec.rb b/spec/requests/api/admin/plan_limits_spec.rb
index 2de7a66d803..6085b48c7c2 100644
--- a/spec/requests/api/admin/plan_limits_spec.rb
+++ b/spec/requests/api/admin/plan_limits_spec.rb
@@ -2,30 +2,22 @@
require 'spec_helper'
-RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owned do
- let_it_be(:user) { create(:user) }
+RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :shared do
let_it_be(:admin) { create(:admin) }
let_it_be(:plan) { create(:plan, name: 'default') }
+ let_it_be(:path) { '/application/plan_limits' }
describe 'GET /application/plan_limits' do
- context 'as a non-admin user' do
- it 'returns 403' do
- get api('/application/plan_limits', user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
+ it_behaves_like 'GET request permissions for admin mode'
context 'as an admin user' do
context 'no params' do
- it 'returns plan limits' do
- get api('/application/plan_limits', admin)
+ it 'returns plan limits', :aggregate_failures do
+ get api(path, admin, admin_mode: true)
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Hash
expect(json_response['ci_pipeline_size']).to eq(Plan.default.actual_limits.ci_pipeline_size)
expect(json_response['ci_active_jobs']).to eq(Plan.default.actual_limits.ci_active_jobs)
- expect(json_response['ci_active_pipelines']).to eq(Plan.default.actual_limits.ci_active_pipelines)
expect(json_response['ci_project_subscriptions']).to eq(Plan.default.actual_limits.ci_project_subscriptions)
expect(json_response['ci_pipeline_schedules']).to eq(Plan.default.actual_limits.ci_pipeline_schedules)
expect(json_response['ci_needs_size_limit']).to eq(Plan.default.actual_limits.ci_needs_size_limit)
@@ -49,14 +41,13 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
@params = { plan_name: 'default' }
end
- it 'returns plan limits' do
- get api('/application/plan_limits', admin), params: @params
+ it 'returns plan limits', :aggregate_failures do
+ get api(path, admin, admin_mode: true), params: @params
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Hash
expect(json_response['ci_pipeline_size']).to eq(Plan.default.actual_limits.ci_pipeline_size)
expect(json_response['ci_active_jobs']).to eq(Plan.default.actual_limits.ci_active_jobs)
- expect(json_response['ci_active_pipelines']).to eq(Plan.default.actual_limits.ci_active_pipelines)
expect(json_response['ci_project_subscriptions']).to eq(Plan.default.actual_limits.ci_project_subscriptions)
expect(json_response['ci_pipeline_schedules']).to eq(Plan.default.actual_limits.ci_pipeline_schedules)
expect(json_response['ci_needs_size_limit']).to eq(Plan.default.actual_limits.ci_needs_size_limit)
@@ -80,8 +71,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
@params = { plan_name: 'my-plan' }
end
- it 'returns validation error' do
- get api('/application/plan_limits', admin), params: @params
+ it 'returns validation error', :aggregate_failures do
+ get api(path, admin, admin_mode: true), params: @params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('plan_name does not have a valid value')
@@ -91,22 +82,17 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
end
describe 'PUT /application/plan_limits' do
- context 'as a non-admin user' do
- it 'returns 403' do
- put api('/application/plan_limits', user), params: { plan_name: 'default' }
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:params) { { 'plan_name': 'default' } }
end
context 'as an admin user' do
context 'correct params' do
- it 'updates multiple plan limits' do
- put api('/application/plan_limits', admin), params: {
+ it 'updates multiple plan limits', :aggregate_failures do
+ put api(path, admin, admin_mode: true), params: {
'plan_name': 'default',
'ci_pipeline_size': 101,
'ci_active_jobs': 102,
- 'ci_active_pipelines': 103,
'ci_project_subscriptions': 104,
'ci_pipeline_schedules': 105,
'ci_needs_size_limit': 106,
@@ -124,11 +110,9 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
'pipeline_hierarchy_size': 250
}
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Hash
expect(json_response['ci_pipeline_size']).to eq(101)
expect(json_response['ci_active_jobs']).to eq(102)
- expect(json_response['ci_active_pipelines']).to eq(103)
expect(json_response['ci_project_subscriptions']).to eq(104)
expect(json_response['ci_pipeline_schedules']).to eq(105)
expect(json_response['ci_needs_size_limit']).to eq(106)
@@ -146,8 +130,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
expect(json_response['pipeline_hierarchy_size']).to eq(250)
end
- it 'updates single plan limits' do
- put api('/application/plan_limits', admin), params: {
+ it 'updates single plan limits', :aggregate_failures do
+ put api(path, admin, admin_mode: true), params: {
'plan_name': 'default',
'maven_max_file_size': 100
}
@@ -159,8 +143,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
end
context 'empty params' do
- it 'fails to update plan limits' do
- put api('/application/plan_limits', admin), params: {}
+ it 'fails to update plan limits', :aggregate_failures do
+ put api(path, admin, admin_mode: true), params: {}
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to match('plan_name is missing')
@@ -168,12 +152,11 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
end
context 'params with wrong type' do
- it 'fails to update plan limits' do
- put api('/application/plan_limits', admin), params: {
+ it 'fails to update plan limits', :aggregate_failures do
+ put api(path, admin, admin_mode: true), params: {
'plan_name': 'default',
'ci_pipeline_size': 'z',
'ci_active_jobs': 'y',
- 'ci_active_pipelines': 'x',
'ci_project_subscriptions': 'w',
'ci_pipeline_schedules': 'v',
'ci_needs_size_limit': 'u',
@@ -195,7 +178,6 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
expect(json_response['error']).to include(
'ci_pipeline_size is invalid',
'ci_active_jobs is invalid',
- 'ci_active_pipelines is invalid',
'ci_project_subscriptions is invalid',
'ci_pipeline_schedules is invalid',
'ci_needs_size_limit is invalid',
@@ -216,8 +198,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
end
context 'missing plan_name in params' do
- it 'fails to update plan limits' do
- put api('/application/plan_limits', admin), params: { 'conan_max_file_size': 0 }
+ it 'fails to update plan limits', :aggregate_failures do
+ put api(path, admin, admin_mode: true), params: { 'conan_max_file_size': 0 }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to match('plan_name is missing')
@@ -229,8 +211,8 @@ RSpec.describe API::Admin::PlanLimits, 'PlanLimits', feature_category: :not_owne
Plan.default.actual_limits.update!({ 'golang_max_file_size': 1000 })
end
- it 'updates only declared plan limits' do
- put api('/application/plan_limits', admin), params: {
+ it 'updates only declared plan limits', :aggregate_failures do
+ put api(path, admin, admin_mode: true), params: {
'plan_name': 'default',
'pypi_max_file_size': 200,
'golang_max_file_size': 999
diff --git a/spec/requests/api/admin/sidekiq_spec.rb b/spec/requests/api/admin/sidekiq_spec.rb
index 0b456721d4f..eca12c8e433 100644
--- a/spec/requests/api/admin/sidekiq_spec.rb
+++ b/spec/requests/api/admin/sidekiq_spec.rb
@@ -2,18 +2,10 @@
require 'spec_helper'
-RSpec.describe API::Admin::Sidekiq, :clean_gitlab_redis_queues, feature_category: :not_owned do
+RSpec.describe API::Admin::Sidekiq, :clean_gitlab_redis_queues, feature_category: :shared do
let_it_be(:admin) { create(:admin) }
describe 'DELETE /admin/sidekiq/queues/:queue_name' do
- context 'when the user is not an admin' do
- it 'returns a 403' do
- delete api("/admin/sidekiq/queues/authorized_projects?user=#{admin.username}", create(:user))
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
context 'when the user is an admin' do
around do |example|
Sidekiq::Queue.new('authorized_projects').clear
@@ -31,14 +23,21 @@ RSpec.describe API::Admin::Sidekiq, :clean_gitlab_redis_queues, feature_category
end
context 'valid request' do
- it 'returns info about the deleted jobs' do
+ before do
add_job(admin, [1])
add_job(admin, [2])
add_job(create(:user), [3])
+ end
+
+ let_it_be(:path) { "/admin/sidekiq/queues/authorized_projects?user=#{admin.username}&worker_class=AuthorizedProjectsWorker" }
- delete api("/admin/sidekiq/queues/authorized_projects?user=#{admin.username}&worker_class=AuthorizedProjectsWorker", admin)
+ it_behaves_like 'DELETE request permissions for admin mode' do
+ let(:success_status_code) { :ok }
+ end
+
+ it 'returns info about the deleted jobs' do
+ delete api(path, admin, admin_mode: true)
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq('completed' => true,
'deleted_jobs' => 2,
'queue_size' => 1)
@@ -47,7 +46,7 @@ RSpec.describe API::Admin::Sidekiq, :clean_gitlab_redis_queues, feature_category
context 'when no required params are provided' do
it 'returns a 400' do
- delete api("/admin/sidekiq/queues/authorized_projects?user_2=#{admin.username}", admin)
+ delete api("/admin/sidekiq/queues/authorized_projects?user_2=#{admin.username}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -55,7 +54,7 @@ RSpec.describe API::Admin::Sidekiq, :clean_gitlab_redis_queues, feature_category
context 'when the queue does not exist' do
it 'returns a 404' do
- delete api("/admin/sidekiq/queues/authorized_projects_2?user=#{admin.username}", admin)
+ delete api("/admin/sidekiq/queues/authorized_projects_2?user=#{admin.username}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/requests/api/api_guard/admin_mode_middleware_spec.rb b/spec/requests/api/api_guard/admin_mode_middleware_spec.rb
index 21f3691c20b..7268fa2c90b 100644
--- a/spec/requests/api/api_guard/admin_mode_middleware_spec.rb
+++ b/spec/requests/api/api_guard/admin_mode_middleware_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::APIGuard::AdminModeMiddleware, :request_store, feature_category: :not_owned do
+RSpec.describe API::APIGuard::AdminModeMiddleware, :request_store, feature_category: :shared do
let(:user) { create(:admin) }
it 'is loaded' do
diff --git a/spec/requests/api/api_guard/response_coercer_middleware_spec.rb b/spec/requests/api/api_guard/response_coercer_middleware_spec.rb
index 77498c2e2b3..4a993d0b255 100644
--- a/spec/requests/api/api_guard/response_coercer_middleware_spec.rb
+++ b/spec/requests/api/api_guard/response_coercer_middleware_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::APIGuard::ResponseCoercerMiddleware, feature_category: :not_owned do
+RSpec.describe API::APIGuard::ResponseCoercerMiddleware, feature_category: :shared do
using RSpec::Parameterized::TableSyntax
it 'is loaded' do
diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb
index 35851fff6c8..219c7dbdbc5 100644
--- a/spec/requests/api/api_spec.rb
+++ b/spec/requests/api/api_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::API, feature_category: :authentication_and_authorization do
+RSpec.describe API::API, feature_category: :system_access do
include GroupAPIHelpers
describe 'Record user last activity in after hook' do
@@ -359,4 +359,26 @@ RSpec.describe API::API, feature_category: :authentication_and_authorization do
end
end
end
+
+ describe 'Handle Gitlab::Git::ResourceExhaustedError exception' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository, creator: user) }
+
+ before do
+ project.add_maintainer(user)
+ allow(Gitlab::GitalyClient).to receive(:call).with(any_args).and_raise(
+ Gitlab::Git::ResourceExhaustedError.new("Upstream Gitaly has been exhausted. Try again later", 50)
+ )
+ end
+
+ it 'returns 429 status with exhausted' do
+ get api("/projects/#{project.id}/repository/commits", user)
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ expect(response.headers['Retry-After']).to be(50)
+ expect(json_response).to eql(
+ 'message' => 'Upstream Gitaly has been exhausted. Try again later'
+ )
+ end
+ end
end
diff --git a/spec/requests/api/appearance_spec.rb b/spec/requests/api/appearance_spec.rb
index c08ecae28e8..2ea4dcce7d8 100644
--- a/spec/requests/api/appearance_spec.rb
+++ b/spec/requests/api/appearance_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do
+RSpec.describe API::Appearance, 'Appearance', :aggregate_failures, feature_category: :navigation do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
let_it_be(:path) { "/application/appearance" }
@@ -12,7 +12,7 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do
context 'as an admin user' do
it "returns appearance" do
- get api("/application/appearance", admin, admin_mode: true)
+ get api(path, admin, admin_mode: true)
expect(json_response).to be_an Hash
expect(json_response['description']).to eq('')
@@ -36,12 +36,14 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do
end
describe "PUT /application/appearance" do
- it_behaves_like 'PUT request permissions for admin mode', { title: "Test" }
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:params) { { title: "Test" } }
+ end
context 'as an admin user' do
context "instance basics" do
it "allows updating the settings" do
- put api("/application/appearance", admin, admin_mode: true), params: {
+ put api(path, admin, admin_mode: true), params: {
title: "GitLab Test Instance",
description: "gitlab-test.example.com",
pwa_name: "GitLab PWA Test",
@@ -81,7 +83,7 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do
email_header_and_footer_enabled: true
}
- put api("/application/appearance", admin, admin_mode: true), params: settings
+ put api(path, admin, admin_mode: true), params: settings
expect(response).to have_gitlab_http_status(:ok)
settings.each do |attribute, value|
@@ -91,14 +93,14 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do
context "fails on invalid color values" do
it "with message_font_color" do
- put api("/application/appearance", admin, admin_mode: true), params: { message_font_color: "No Color" }
+ put api(path, admin, admin_mode: true), params: { message_font_color: "No Color" }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['message_font_color']).to contain_exactly('must be a valid color code')
end
it "with message_background_color" do
- put api("/application/appearance", admin, admin_mode: true), params: { message_background_color: "#1" }
+ put api(path, admin, admin_mode: true), params: { message_background_color: "#1" }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['message_background_color']).to contain_exactly('must be a valid color code')
@@ -110,7 +112,7 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do
let_it_be(:appearance) { create(:appearance) }
it "allows updating the image files" do
- put api("/application/appearance", admin, admin_mode: true), params: {
+ put api(path, admin, admin_mode: true), params: {
logo: fixture_file_upload("spec/fixtures/dk.png", "image/png"),
header_logo: fixture_file_upload("spec/fixtures/dk.png", "image/png"),
pwa_icon: fixture_file_upload("spec/fixtures/dk.png", "image/png"),
@@ -126,14 +128,14 @@ RSpec.describe API::Appearance, 'Appearance', feature_category: :navigation do
context "fails on invalid color images" do
it "with string instead of file" do
- put api("/application/appearance", admin, admin_mode: true), params: { logo: 'not-a-file.png' }
+ put api(path, admin, admin_mode: true), params: { logo: 'not-a-file.png' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq("logo is invalid")
end
it "with .svg file instead of .png" do
- put api("/application/appearance", admin, admin_mode: true), params: { favicon: fixture_file_upload("spec/fixtures/logo_sample.svg", "image/svg") }
+ put api(path, admin, admin_mode: true), params: { favicon: fixture_file_upload("spec/fixtures/logo_sample.svg", "image/svg") }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['favicon']).to contain_exactly("You are not allowed to upload \"svg\" files, allowed types: png, ico")
diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb
index b81cdcfea8e..16e24807e67 100644
--- a/spec/requests/api/applications_spec.rb
+++ b/spec/requests/api/applications_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Applications, :api, feature_category: :authentication_and_authorization do
+RSpec.describe API::Applications, :aggregate_failures, :api, feature_category: :system_access do
let_it_be(:admin) { create(:admin) }
let_it_be(:user) { create(:user) }
let_it_be(:scopes) { 'api' }
@@ -10,7 +10,9 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
let!(:application) { create(:application, name: 'another_application', owner: nil, redirect_uri: 'http://other_application.url', scopes: scopes) }
describe 'POST /applications' do
- it_behaves_like 'POST request permissions for admin mode', { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'api' }
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { { name: 'application_name', redirect_uri: 'http://application.url', scopes: 'api' } }
+ end
context 'authenticated and authorized user' do
it 'creates and returns an OAuth application' do
@@ -22,7 +24,7 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
expect(json_response).to be_a Hash
expect(json_response['application_id']).to eq application.uid
- expect(json_response['secret']).to eq application.secret
+ expect(application.secret_matches?(json_response['secret'])).to eq(true)
expect(json_response['callback_url']).to eq application.redirect_uri
expect(json_response['confidential']).to eq application.confidential
expect(application.scopes.to_s).to eq('api')
@@ -133,7 +135,7 @@ RSpec.describe API::Applications, :api, feature_category: :authentication_and_au
context 'authorized user without authorization' do
it 'does not create application' do
expect do
- post api('/applications', user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: scopes }
+ post api(path, user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: scopes }
end.not_to change { Doorkeeper::Application.count }
end
end
diff --git a/spec/requests/api/avatar_spec.rb b/spec/requests/api/avatar_spec.rb
index fcef5b6ca78..0a77b6e228e 100644
--- a/spec/requests/api/avatar_spec.rb
+++ b/spec/requests/api/avatar_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe API::Avatar, feature_category: :user_profile do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['avatar_url']).to eql("#{::Settings.gitlab.base_url}#{user.avatar.local_url}")
+ is_expected.to have_request_urgency(:medium)
end
end
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 87dc06b7d15..22c67a253e3 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::AwardEmoji, feature_category: :not_owned do
+RSpec.describe API::AwardEmoji, feature_category: :shared do
let_it_be_with_reload(:project) { create(:project, :private) }
let_it_be(:user) { create(:user) }
let_it_be(:issue) { create(:issue, project: project) }
diff --git a/spec/requests/api/badges_spec.rb b/spec/requests/api/badges_spec.rb
index 6c6a7cc7cc6..1c09c1129a2 100644
--- a/spec/requests/api/badges_spec.rb
+++ b/spec/requests/api/badges_spec.rb
@@ -72,9 +72,9 @@ RSpec.describe API::Badges, feature_category: :projects do
context 'when authenticated as a non-member' do
%i[maintainer developer access_requester stranger].each do |type|
- let(:badge) { source.badges.first }
-
context "as a #{type}" do
+ let(:badge) { source.badges.first }
+
it 'returns 200', :quarantine do
user = public_send(type)
diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb
index 5cbb7dbfa12..530c81364a8 100644
--- a/spec/requests/api/broadcast_messages_spec.rb
+++ b/spec/requests/api/broadcast_messages_spec.rb
@@ -2,16 +2,16 @@
require 'spec_helper'
-RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
- let_it_be(:user) { create(:user) }
+RSpec.describe API::BroadcastMessages, :aggregate_failures, feature_category: :onboarding do
let_it_be(:admin) { create(:admin) }
let_it_be(:message) { create(:broadcast_message) }
+ let_it_be(:path) { '/broadcast_messages' }
describe 'GET /broadcast_messages' do
it 'returns an Array of BroadcastMessages' do
create(:broadcast_message)
- get api('/broadcast_messages')
+ get api(path)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -22,8 +22,10 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
end
describe 'GET /broadcast_messages/:id' do
+ let_it_be(:path) { "#{path}/#{message.id}" }
+
it 'returns the specified message' do
- get api("/broadcast_messages/#{message.id}")
+ get api(path)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq message.id
@@ -33,16 +35,14 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
end
describe 'POST /broadcast_messages' do
- it 'returns a 401 for anonymous users' do
- post api('/broadcast_messages'), params: attributes_for(:broadcast_message)
-
- expect(response).to have_gitlab_http_status(:unauthorized)
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { { message: 'Test message' } }
end
- it 'returns a 403 for users' do
- post api('/broadcast_messages', user), params: attributes_for(:broadcast_message)
+ it 'returns a 401 for anonymous users' do
+ post api(path), params: attributes_for(:broadcast_message)
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
context 'as an admin' do
@@ -50,7 +50,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
attrs = attributes_for(:broadcast_message)
attrs.delete(:message)
- post api('/broadcast_messages', admin), params: attrs
+ post api(path, admin, admin_mode: true), params: attrs
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq 'message is missing'
@@ -59,7 +59,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
it 'defines sane default start and end times' do
time = Time.zone.parse('2016-07-02 10:11:12')
travel_to(time) do
- post api('/broadcast_messages', admin), params: { message: 'Test message' }
+ post api(path, admin, admin_mode: true), params: { message: 'Test message' }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z'
@@ -70,7 +70,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
it 'accepts a custom background and foreground color' do
attrs = attributes_for(:broadcast_message, color: '#000000', font: '#cecece')
- post api('/broadcast_messages', admin), params: attrs
+ post api(path, admin, admin_mode: true), params: attrs
expect(response).to have_gitlab_http_status(:created)
expect(json_response['color']).to eq attrs[:color]
@@ -81,7 +81,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
target_access_levels = [Gitlab::Access::GUEST, Gitlab::Access::DEVELOPER]
attrs = attributes_for(:broadcast_message, target_access_levels: target_access_levels)
- post api('/broadcast_messages', admin), params: attrs
+ post api(path, admin, admin_mode: true), params: attrs
expect(response).to have_gitlab_http_status(:created)
expect(json_response['target_access_levels']).to eq attrs[:target_access_levels]
@@ -90,7 +90,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
it 'accepts a target path' do
attrs = attributes_for(:broadcast_message, target_path: "*/welcome")
- post api('/broadcast_messages', admin), params: attrs
+ post api(path, admin, admin_mode: true), params: attrs
expect(response).to have_gitlab_http_status(:created)
expect(json_response['target_path']).to eq attrs[:target_path]
@@ -99,7 +99,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
it 'accepts a broadcast type' do
attrs = attributes_for(:broadcast_message, broadcast_type: 'notification')
- post api('/broadcast_messages', admin), params: attrs
+ post api(path, admin, admin_mode: true), params: attrs
expect(response).to have_gitlab_http_status(:created)
expect(json_response['broadcast_type']).to eq attrs[:broadcast_type]
@@ -108,7 +108,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
it 'uses default broadcast type' do
attrs = attributes_for(:broadcast_message)
- post api('/broadcast_messages', admin), params: attrs
+ post api(path, admin, admin_mode: true), params: attrs
expect(response).to have_gitlab_http_status(:created)
expect(json_response['broadcast_type']).to eq 'banner'
@@ -117,7 +117,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
it 'errors for invalid broadcast type' do
attrs = attributes_for(:broadcast_message, broadcast_type: 'invalid-type')
- post api('/broadcast_messages', admin), params: attrs
+ post api(path, admin, admin_mode: true), params: attrs
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -125,7 +125,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
it 'accepts an active dismissable value' do
attrs = { message: 'new message', dismissable: true }
- post api('/broadcast_messages', admin), params: attrs
+ post api(path, admin, admin_mode: true), params: attrs
expect(response).to have_gitlab_http_status(:created)
expect(json_response['dismissable']).to eq true
@@ -134,27 +134,25 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
end
describe 'PUT /broadcast_messages/:id' do
- it 'returns a 401 for anonymous users' do
- put api("/broadcast_messages/#{message.id}"),
- params: attributes_for(:broadcast_message)
+ let_it_be(:path) { "#{path}/#{message.id}" }
- expect(response).to have_gitlab_http_status(:unauthorized)
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:params) { { message: 'Test message' } }
end
- it 'returns a 403 for users' do
- put api("/broadcast_messages/#{message.id}", user),
+ it 'returns a 401 for anonymous users' do
+ put api(path),
params: attributes_for(:broadcast_message)
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
context 'as an admin' do
it 'accepts new background and foreground colors' do
attrs = { color: '#000000', font: '#cecece' }
- put api("/broadcast_messages/#{message.id}", admin), params: attrs
+ put api(path, admin, admin_mode: true), params: attrs
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response['color']).to eq attrs[:color]
expect(json_response['font']).to eq attrs[:font]
end
@@ -164,7 +162,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
travel_to(time) do
attrs = { starts_at: Time.zone.now, ends_at: 3.hours.from_now }
- put api("/broadcast_messages/#{message.id}", admin), params: attrs
+ put api(path, admin, admin_mode: true), params: attrs
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z'
@@ -175,7 +173,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
it 'accepts a new message' do
attrs = { message: 'new message' }
- put api("/broadcast_messages/#{message.id}", admin), params: attrs
+ put api(path, admin, admin_mode: true), params: attrs
expect(response).to have_gitlab_http_status(:ok)
expect { message.reload }.to change { message.message }.to('new message')
@@ -184,7 +182,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
it 'accepts a new target_access_levels' do
attrs = { target_access_levels: [Gitlab::Access::MAINTAINER] }
- put api("/broadcast_messages/#{message.id}", admin), params: attrs
+ put api(path, admin, admin_mode: true), params: attrs
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['target_access_levels']).to eq attrs[:target_access_levels]
@@ -193,7 +191,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
it 'accepts a new target_path' do
attrs = { target_path: '*/welcome' }
- put api("/broadcast_messages/#{message.id}", admin), params: attrs
+ put api(path, admin, admin_mode: true), params: attrs
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['target_path']).to eq attrs[:target_path]
@@ -202,7 +200,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
it 'accepts a new broadcast_type' do
attrs = { broadcast_type: 'notification' }
- put api("/broadcast_messages/#{message.id}", admin), params: attrs
+ put api(path, admin, admin_mode: true), params: attrs
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['broadcast_type']).to eq attrs[:broadcast_type]
@@ -211,7 +209,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
it 'errors for invalid broadcast type' do
attrs = { broadcast_type: 'invalid-type' }
- put api("/broadcast_messages/#{message.id}", admin), params: attrs
+ put api(path, admin, admin_mode: true), params: attrs
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -219,7 +217,7 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
it 'accepts a new dismissable value' do
attrs = { message: 'new message', dismissable: true }
- put api("/broadcast_messages/#{message.id}", admin), params: attrs
+ put api(path, admin, admin_mode: true), params: attrs
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['dismissable']).to eq true
@@ -228,27 +226,24 @@ RSpec.describe API::BroadcastMessages, feature_category: :onboarding do
end
describe 'DELETE /broadcast_messages/:id' do
- it 'returns a 401 for anonymous users' do
- delete api("/broadcast_messages/#{message.id}"),
- params: attributes_for(:broadcast_message)
+ let_it_be(:path) { "#{path}/#{message.id}" }
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
+ it_behaves_like 'DELETE request permissions for admin mode'
- it 'returns a 403 for users' do
- delete api("/broadcast_messages/#{message.id}", user),
+ it 'returns a 401 for anonymous users' do
+ delete api(path),
params: attributes_for(:broadcast_message)
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
it_behaves_like '412 response' do
- let(:request) { api("/broadcast_messages/#{message.id}", admin) }
+ let(:request) { api("/broadcast_messages/#{message.id}", admin, admin_mode: true) }
end
it 'deletes the broadcast message for admins' do
expect do
- delete api("/broadcast_messages/#{message.id}", admin)
+ delete api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
end.to change { BroadcastMessage.count }.by(-1)
diff --git a/spec/requests/api/bulk_imports_spec.rb b/spec/requests/api/bulk_imports_spec.rb
index 23dfe865ba3..b159d4ad445 100644
--- a/spec/requests/api/bulk_imports_spec.rb
+++ b/spec/requests/api/bulk_imports_spec.rb
@@ -75,6 +75,8 @@ RSpec.describe API::BulkImports, feature_category: :importers do
end
describe 'POST /bulk_imports' do
+ let_it_be(:destination_namespace) { create(:group) }
+
let(:request) { post api('/bulk_imports', user), params: params }
let(:destination_param) { { destination_slug: 'destination_slug' } }
let(:params) do
@@ -87,12 +89,15 @@ RSpec.describe API::BulkImports, feature_category: :importers do
{
source_type: 'group_entity',
source_full_path: 'full_path',
- destination_namespace: 'destination_namespace'
+ destination_namespace: destination_namespace.path
}.merge(destination_param)
]
}
end
+ let(:source_entity_type) { BulkImports::CreateService::ENTITY_TYPES_MAPPING.fetch(params[:entities][0][:source_type]) }
+ let(:source_entity_identifier) { ERB::Util.url_encode(params[:entities][0][:source_full_path]) }
+
before do
allow_next_instance_of(BulkImports::Clients::HTTP) do |instance|
allow(instance)
@@ -103,6 +108,10 @@ RSpec.describe API::BulkImports, feature_category: :importers do
.to receive(:instance_enterprise)
.and_return(false)
end
+ stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=access_token")
+ .to_return(status: 200, body: "", headers: {})
+
+ destination_namespace.add_owner(user)
end
shared_examples 'starting a new migration' do
@@ -192,7 +201,7 @@ RSpec.describe API::BulkImports, feature_category: :importers do
{
source_type: 'group_entity',
source_full_path: 'full_path',
- destination_namespace: 'destination_namespace'
+ destination_namespace: destination_namespace.path
}
]
}
@@ -214,20 +223,17 @@ RSpec.describe API::BulkImports, feature_category: :importers do
request
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq("entities[0][source_full_path] must be a relative path and not include protocol, sub-domain, " \
- "or domain information. E.g. 'source/full/path' not 'https://example.com/source/full/path'")
+ "or domain information. For example, 'source/full/path' not 'https://example.com/source/full/path'")
end
end
- context 'when the destination_namespace is invalid' do
+ context 'when the destination_namespace does not exist' do
it 'returns invalid error' do
- params[:entities][0][:destination_namespace] = "?not a destination-namespace"
+ params[:entities][0][:destination_namespace] = "invalid-destination-namespace"
request
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq("entities[0][destination_namespace] cannot start with a dash or forward slash, " \
- "or end with a period or forward slash. It can only contain alphanumeric " \
- "characters, periods, underscores, forward slashes and dashes. " \
- "E.g. 'destination_namespace' or 'destination/namespace'")
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response['message']).to eq("Import failed. Destination 'invalid-destination-namespace' is invalid, or you don't have permission.")
end
end
@@ -243,15 +249,35 @@ RSpec.describe API::BulkImports, feature_category: :importers do
end
context 'when the destination_slug is invalid' do
- it 'returns invalid error' do
+ it 'returns invalid error when restricting special characters is disabled' do
+ Feature.disable(:restrict_special_characters_in_namespace_path)
+
+ params[:entities][0][:destination_slug] = 'des?tin?atoi-slugg'
+
+ request
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to include("entities[0][destination_slug] cannot start with " \
+ "a non-alphanumeric character except for periods or " \
+ "underscores, can contain only alphanumeric characters, " \
+ "periods, and underscores, cannot end with a period or " \
+ "forward slash, and has no leading or trailing forward " \
+ "slashes. It can only contain alphanumeric characters, " \
+ "periods, underscores, and dashes. For example, " \
+ "'destination_namespace' not 'destination/namespace'")
+ end
+
+ it 'returns invalid error when restricting special characters is enabled' do
+ Feature.enable(:restrict_special_characters_in_namespace_path)
+
params[:entities][0][:destination_slug] = 'des?tin?atoi-slugg'
request
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to include("entities[0][destination_slug] cannot start with a dash " \
- "or forward slash, or end with a period or forward slash. " \
- "It can only contain alphanumeric characters, periods, underscores, and dashes. " \
- "E.g. 'destination_namespace' not 'destination/namespace'")
+ expect(json_response['error']).to include("entities[0][destination_slug] must not start or " \
+ "end with a special character and must not contain " \
+ "consecutive special characters. It can only contain " \
+ "alphanumeric characters, periods, underscores, and " \
+ "dashes. For example, 'destination_namespace' not 'destination/namespace'")
end
end
@@ -271,12 +297,41 @@ RSpec.describe API::BulkImports, feature_category: :importers do
}
end
+ it 'returns blocked url message in the error' do
+ request
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+
+ expect(json_response['message']).to include("Url is blocked: Only allowed schemes are http, https")
+ end
+ end
+
+ context 'when source instance setting is disabled' do
+ let(:params) do
+ {
+ configuration: {
+ url: 'http://gitlab.example',
+ access_token: 'access_token'
+ },
+ entities: [
+ source_type: 'group_entity',
+ source_full_path: 'full_path',
+ destination_slug: 'destination_slug',
+ destination_namespace: 'destination_namespace'
+ ]
+ }
+ end
+
it 'returns blocked url error' do
+ stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=access_token")
+ .to_return(status: 404, body: "", headers: {})
+
request
expect(response).to have_gitlab_http_status(:unprocessable_entity)
- expect(json_response['message']).to eq('Validation failed: Url is blocked: Only allowed schemes are http, https')
+ expect(json_response['message']).to include("Group import disabled on source or destination instance. " \
+ "Ask an administrator to enable it on both instances and try again.")
end
end
diff --git a/spec/requests/api/ci/job_artifacts_spec.rb b/spec/requests/api/ci/job_artifacts_spec.rb
index ee390773f29..7cea744cdb9 100644
--- a/spec/requests/api/ci/job_artifacts_spec.rb
+++ b/spec/requests/api/ci/job_artifacts_spec.rb
@@ -190,7 +190,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
end
context 'when project is public with artifacts that are non public' do
- let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline) }
it 'rejects access to artifacts' do
project.update_column(:visibility_level,
@@ -439,7 +439,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
context 'when public project guest and artifacts are non public' do
let(:api_user) { guest }
- let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline) }
before do
project.update_column(:visibility_level,
@@ -644,7 +644,7 @@ RSpec.describe API::Ci::JobArtifacts, feature_category: :build_artifacts do
end
context 'when project is public with non public artifacts' do
- let(:job) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline, user: api_user) }
+ let(:job) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline, user: api_user) }
let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
let(:public_builds) { true }
diff --git a/spec/requests/api/ci/jobs_spec.rb b/spec/requests/api/ci/jobs_spec.rb
index 8b3ec59b785..ed0cec46a42 100644
--- a/spec/requests/api/ci/jobs_spec.rb
+++ b/spec/requests/api/ci/jobs_spec.rb
@@ -198,22 +198,22 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
let_it_be(:agent_authorizations_without_env) do
[
- create(:agent_group_authorization, agent: create(:cluster_agent, project: other_project), group: group),
- create(:agent_project_authorization, agent: create(:cluster_agent, project: project), project: project),
- Clusters::Agents::ImplicitAuthorization.new(agent: create(:cluster_agent, project: project))
+ create(:agent_ci_access_group_authorization, agent: create(:cluster_agent, project: other_project), group: group),
+ create(:agent_ci_access_project_authorization, agent: create(:cluster_agent, project: project), project: project),
+ Clusters::Agents::Authorizations::CiAccess::ImplicitAuthorization.new(agent: create(:cluster_agent, project: project))
]
end
let_it_be(:agent_authorizations_with_review_and_production_env) do
[
create(
- :agent_group_authorization,
+ :agent_ci_access_group_authorization,
agent: create(:cluster_agent, project: other_project),
group: group,
environments: ['production', 'review/*']
),
create(
- :agent_project_authorization,
+ :agent_ci_access_project_authorization,
agent: create(:cluster_agent, project: project),
project: project,
environments: ['production', 'review/*']
@@ -224,13 +224,13 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
let_it_be(:agent_authorizations_with_staging_env) do
[
create(
- :agent_group_authorization,
+ :agent_ci_access_group_authorization,
agent: create(:cluster_agent, project: other_project),
group: group,
environments: ['staging']
),
create(
- :agent_project_authorization,
+ :agent_ci_access_project_authorization,
agent: create(:cluster_agent, project: project),
project: project,
environments: ['staging']
@@ -546,40 +546,18 @@ RSpec.describe API::Ci::Jobs, feature_category: :continuous_integration do
describe 'GET /projects/:id/jobs rate limited' do
let(:query) { {} }
- context 'with the ci_enforce_rate_limits_jobs_api feature flag on' do
- before do
- stub_feature_flags(ci_enforce_rate_limits_jobs_api: true)
-
- allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy|
- threshold = Gitlab::ApplicationRateLimiter.rate_limits[:jobs_index][:threshold]
- allow(strategy).to receive(:increment).and_return(threshold + 1)
- end
-
- get api("/projects/#{project.id}/jobs", api_user), params: query
+ before do
+ allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy|
+ threshold = Gitlab::ApplicationRateLimiter.rate_limits[:jobs_index][:threshold]
+ allow(strategy).to receive(:increment).and_return(threshold + 1)
end
- it 'enforces rate limits for the endpoint' do
- expect(response).to have_gitlab_http_status :too_many_requests
- expect(json_response['message']['error']).to eq('This endpoint has been requested too many times. Try again later.')
- end
+ get api("/projects/#{project.id}/jobs", api_user), params: query
end
- context 'with the ci_enforce_rate_limits_jobs_api feature flag off' do
- before do
- stub_feature_flags(ci_enforce_rate_limits_jobs_api: false)
-
- allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy|
- threshold = Gitlab::ApplicationRateLimiter.rate_limits[:jobs_index][:threshold]
- allow(strategy).to receive(:increment).and_return(threshold + 1)
- end
-
- get api("/projects/#{project.id}/jobs", api_user), params: query
- end
-
- it 'makes a successful request' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_limited_pagination_headers
- end
+ it 'enforces rate limits for the endpoint' do
+ expect(response).to have_gitlab_http_status :too_many_requests
+ expect(json_response['message']['error']).to eq('This endpoint has been requested too many times. Try again later.')
end
end
diff --git a/spec/requests/api/ci/pipeline_schedules_spec.rb b/spec/requests/api/ci/pipeline_schedules_spec.rb
index 2a2c5f65aee..d760e4ddf28 100644
--- a/spec/requests/api/ci/pipeline_schedules_spec.rb
+++ b/spec/requests/api/ci/pipeline_schedules_spec.rb
@@ -473,12 +473,12 @@ RSpec.describe API::Ci::PipelineSchedules, feature_category: :continuous_integra
end
context 'as the existing owner of the schedule' do
- it 'rejects the request and leaves the schedule unchanged' do
+ it 'accepts the request and leaves the schedule unchanged' do
expect do
post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership", developer)
end.not_to change { pipeline_schedule.reload.owner }
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:success)
end
end
end
diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb
index 6d69da85449..869b0ec9dca 100644
--- a/spec/requests/api/ci/pipelines_spec.rb
+++ b/spec/requests/api/ci/pipelines_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let_it_be(:pipeline) do
create(:ci_empty_pipeline, project: project, sha: project.commit.id,
- ref: project.default_branch, user: user)
+ ref: project.default_branch, user: user, name: 'Build pipeline')
end
before do
@@ -25,7 +25,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
it_behaves_like 'pipelines visibility table'
context 'authorized user' do
- it 'returns project pipelines' do
+ it 'returns project pipelines', :aggregate_failures do
get api("/projects/#{project.id}/pipelines", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -41,8 +41,44 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
it 'includes pipeline source' do
get api("/projects/#{project.id}/pipelines", user)
- expect(json_response.first.keys).to contain_exactly(*%w[id iid project_id sha ref status web_url created_at updated_at source])
+ expect(json_response.first.keys).to contain_exactly(*%w[id iid project_id sha ref status web_url created_at updated_at source name])
end
+
+ context 'when pipeline_name_in_api feature flag is off' do
+ before do
+ stub_feature_flags(pipeline_name_in_api: false)
+ end
+
+ it 'does not include pipeline name in response and ignores name parameter' do
+ get api("/projects/#{project.id}/pipelines", user), params: { name: 'Chatops pipeline' }
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.first.keys).not_to include('name')
+ end
+ end
+ end
+
+ it 'avoids N+1 queries' do
+ # Call to trigger any one time queries
+ get api("/projects/#{project.id}/pipelines", user), params: {}
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get api("/projects/#{project.id}/pipelines", user), params: {}
+ end
+
+ 3.times do
+ create(
+ :ci_empty_pipeline,
+ project: project,
+ sha: project.commit.id,
+ ref: project.default_branch,
+ user: user,
+ name: 'Build pipeline')
+ end
+
+ expect do
+ get api("/projects/#{project.id}/pipelines", user), params: {}
+ end.not_to exceed_all_query_limit(control)
end
context 'when parameter is passed' do
@@ -52,7 +88,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
create(:ci_pipeline, project: project, status: target)
end
- it 'returns matched pipelines' do
+ it 'returns matched pipelines', :aggregate_failures do
get api("/projects/#{project.id}/pipelines", user), params: { scope: target }
expect(response).to have_gitlab_http_status(:ok)
@@ -303,11 +339,24 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
end
end
+
+ context 'when name is provided' do
+ let_it_be(:pipeline2) { create(:ci_empty_pipeline, project: project, user: user, name: 'Chatops pipeline') }
+
+ it 'filters by name' do
+ get api("/projects/#{project.id}/pipelines", user), params: { name: 'Build pipeline' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['name']).to eq('Build pipeline')
+ end
+ end
end
end
context 'unauthorized user' do
- it 'does not return project pipelines' do
+ it 'does not return project pipelines', :aggregate_failures do
get api("/projects/#{project.id}/pipelines", non_member)
expect(response).to have_gitlab_http_status(:not_found)
@@ -335,13 +384,13 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'authorized user' do
- it 'returns pipeline jobs' do
+ it 'returns pipeline jobs', :aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
end
- it 'returns correct values' do
+ it 'returns correct values', :aggregate_failures do
expect(json_response).not_to be_empty
expect(json_response.first['commit']['id']).to eq project.commit.id
expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
@@ -354,7 +403,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let(:api_endpoint) { "/projects/#{project.id}/pipelines/#{pipeline.id}/jobs" }
end
- it 'returns pipeline data' do
+ it 'returns pipeline data', :aggregate_failures do
json_job = json_response.first
expect(json_job['pipeline']).not_to be_empty
@@ -368,7 +417,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'filter jobs with one scope element' do
let(:query) { { 'scope' => 'pending' } }
- it do
+ it :aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
@@ -382,7 +431,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'when filtering to only running jobs' do
let(:query) { { 'scope' => 'running' } }
- it do
+ it :aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
@@ -402,7 +451,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'filter jobs with array of scope elements' do
let(:query) { { scope: %w(pending running) } }
- it do
+ it :aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
end
@@ -442,7 +491,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let_it_be(:successor) { create(:ci_build, :success, name: 'build', pipeline: pipeline) }
- it 'does not return retried jobs by default' do
+ it 'does not return retried jobs by default', :aggregate_failures do
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
@@ -450,7 +499,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'when include_retried is false' do
let(:query) { { include_retried: false } }
- it 'does not return retried jobs' do
+ it 'does not return retried jobs', :aggregate_failures do
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
@@ -459,7 +508,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'when include_retried is true' do
let(:query) { { include_retried: true } }
- it 'returns retried jobs' do
+ it 'returns retried jobs', :aggregate_failures do
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response[0]['name']).to eq(json_response[1]['name'])
@@ -469,7 +518,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'no pipeline is found' do
- it 'does not return jobs' do
+ it 'does not return jobs', :aggregate_failures do
get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/jobs", user)
expect(json_response['message']).to eq '404 Project Not Found'
@@ -481,7 +530,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'when user is not logged in' do
let(:api_user) { nil }
- it 'does not return jobs' do
+ it 'does not return jobs', :aggregate_failures do
expect(json_response['message']).to eq '404 Project Not Found'
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -523,13 +572,13 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'authorized user' do
- it 'returns pipeline bridges' do
+ it 'returns pipeline bridges', :aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
end
- it 'returns correct values' do
+ it 'returns correct values', :aggregate_failures do
expect(json_response).not_to be_empty
expect(json_response.first['commit']['id']).to eq project.commit.id
expect(json_response.first['id']).to eq bridge.id
@@ -537,7 +586,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
expect(json_response.first['stage']).to eq bridge.stage
end
- it 'returns pipeline data' do
+ it 'returns pipeline data', :aggregate_failures do
json_bridge = json_response.first
expect(json_bridge['pipeline']).not_to be_empty
@@ -548,7 +597,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
expect(json_bridge['pipeline']['status']).to eq bridge.pipeline.status
end
- it 'returns downstream pipeline data' do
+ it 'returns downstream pipeline data', :aggregate_failures do
json_bridge = json_response.first
expect(json_bridge['downstream_pipeline']).not_to be_empty
@@ -568,7 +617,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'with one scope element' do
let(:query) { { 'scope' => 'pending' } }
- it :skip_before_request do
+ it :skip_before_request, :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
expect(response).to have_gitlab_http_status(:ok)
@@ -581,7 +630,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'with array of scope elements' do
let(:query) { { scope: %w(pending running) } }
- it :skip_before_request do
+ it :skip_before_request, :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/bridges", api_user), params: query
expect(response).to have_gitlab_http_status(:ok)
@@ -635,7 +684,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'no pipeline is found' do
- it 'does not return bridges' do
+ it 'does not return bridges', :aggregate_failures do
get api("/projects/#{project2.id}/pipelines/#{pipeline.id}/bridges", user)
expect(json_response['message']).to eq '404 Project Not Found'
@@ -647,7 +696,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'when user is not logged in' do
let(:api_user) { nil }
- it 'does not return bridges' do
+ it 'does not return bridges', :aggregate_failures do
expect(json_response['message']).to eq '404 Project Not Found'
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -704,7 +753,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
stub_ci_pipeline_to_return_yaml_file
end
- it 'creates and returns a new pipeline' do
+ it 'creates and returns a new pipeline', :aggregate_failures do
expect do
post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch }
end.to change { project.ci_pipelines.count }.by(1)
@@ -717,7 +766,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'variables given' do
let(:variables) { [{ 'variable_type' => 'file', 'key' => 'UPLOAD_TO_S3', 'value' => 'true' }] }
- it 'creates and returns a new pipeline using the given variables' do
+ it 'creates and returns a new pipeline using the given variables', :aggregate_failures do
expect do
post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch, variables: variables }
end.to change { project.ci_pipelines.count }.by(1)
@@ -738,7 +787,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
stub_ci_pipeline_yaml_file(config)
end
- it 'creates and returns a new pipeline using the given variables' do
+ it 'creates and returns a new pipeline using the given variables', :aggregate_failures do
expect do
post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch, variables: variables }
end.to change { project.ci_pipelines.count }.by(1)
@@ -763,7 +812,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
end
- it 'fails when using an invalid ref' do
+ it 'fails when using an invalid ref', :aggregate_failures do
post api("/projects/#{project.id}/pipeline", user), params: { ref: 'invalid_ref' }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -778,7 +827,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
project.update!(auto_devops_attributes: { enabled: false })
end
- it 'fails to create pipeline' do
+ it 'fails to create pipeline', :aggregate_failures do
post api("/projects/#{project.id}/pipeline", user), params: { ref: project.default_branch }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -790,7 +839,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'unauthorized user' do
- it 'does not create pipeline' do
+ it 'does not create pipeline', :aggregate_failures do
post api("/projects/#{project.id}/pipeline", non_member), params: { ref: project.default_branch }
expect(response).to have_gitlab_http_status(:not_found)
@@ -811,21 +860,22 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'authorized user' do
- it 'exposes known attributes' do
+ it 'exposes known attributes', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/pipeline/detail')
end
- it 'returns project pipeline' do
+ it 'returns project pipeline', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['sha']).to match(/\A\h{40}\z/)
+ expect(json_response['name']).to eq('Build pipeline')
end
- it 'returns 404 when it does not exist' do
+ it 'returns 404 when it does not exist', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{non_existing_record_id}", user)
expect(response).to have_gitlab_http_status(:not_found)
@@ -844,10 +894,23 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
expect(json_response["coverage"]).to eq('30.00')
end
end
+
+ context 'with pipeline_name_in_api disabled' do
+ before do
+ stub_feature_flags(pipeline_name_in_api: false)
+ end
+
+ it 'does not return name', :aggregate_failures do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.keys).not_to include('name')
+ end
+ end
end
context 'unauthorized user' do
- it 'does not return a project pipeline' do
+ it 'does not return a project pipeline', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
expect(response).to have_gitlab_http_status(:not_found)
@@ -863,7 +926,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
create(:ci_pipeline, source: dangling_source, project: project)
end
- it 'returns the specified pipeline' do
+ it 'returns the specified pipeline', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{dangling_pipeline.id}", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -878,7 +941,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let!(:second_pipeline) do
create(:ci_empty_pipeline, project: project, sha: second_branch.target,
- ref: second_branch.name, user: user)
+ ref: second_branch.name, user: user, name: 'Build pipeline')
end
before do
@@ -887,18 +950,19 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'default repository branch' do
- it 'gets the latest pipleine' do
+ it 'gets the latest pipleine', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/latest", user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/pipeline/detail')
expect(json_response['ref']).to eq(project.default_branch)
expect(json_response['sha']).to eq(project.commit.id)
+ expect(json_response['name']).to eq('Build pipeline')
end
end
context 'ref parameter' do
- it 'gets the latest pipleine' do
+ it 'gets the latest pipleine', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/latest", user), params: { ref: second_branch.name }
expect(response).to have_gitlab_http_status(:ok)
@@ -907,10 +971,23 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
expect(json_response['sha']).to eq(second_branch.target)
end
end
+
+ context 'with pipeline_name_in_api disabled' do
+ before do
+ stub_feature_flags(pipeline_name_in_api: false)
+ end
+
+ it 'does not return name', :aggregate_failures do
+ get api("/projects/#{project.id}/pipelines/latest", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.keys).not_to include('name')
+ end
+ end
end
context 'unauthorized user' do
- it 'does not return a project pipeline' do
+ it 'does not return a project pipeline', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
expect(response).to have_gitlab_http_status(:not_found)
@@ -926,7 +1003,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let(:api_user) { user }
context 'user is a mantainer' do
- it 'returns pipeline variables empty' do
+ it 'returns pipeline variables empty', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -936,7 +1013,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'with variables' do
let!(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'foo', value: 'bar') }
- it 'returns pipeline variables' do
+ it 'returns pipeline variables', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -962,7 +1039,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let(:api_user) { pipeline_owner_user }
let!(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'foo', value: 'bar') }
- it 'returns pipeline variables' do
+ it 'returns pipeline variables', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -987,7 +1064,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'user is not a project member' do
- it 'does not return pipeline variables' do
+ it 'does not return pipeline variables', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/variables", non_member)
expect(response).to have_gitlab_http_status(:not_found)
@@ -1000,14 +1077,14 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'authorized user' do
let(:owner) { project.first_owner }
- it 'destroys the pipeline' do
+ it 'destroys the pipeline', :aggregate_failures do
delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner)
expect(response).to have_gitlab_http_status(:no_content)
expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
- it 'returns 404 when it does not exist' do
+ it 'returns 404 when it does not exist', :aggregate_failures do
delete api("/projects/#{project.id}/pipelines/#{non_existing_record_id}", owner)
expect(response).to have_gitlab_http_status(:not_found)
@@ -1021,7 +1098,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'when the pipeline has jobs' do
let_it_be(:build) { create(:ci_build, project: project, pipeline: pipeline) }
- it 'destroys associated jobs' do
+ it 'destroys associated jobs', :aggregate_failures do
delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner)
expect(response).to have_gitlab_http_status(:no_content)
@@ -1044,7 +1121,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'unauthorized user' do
context 'when user is not member' do
- it 'returns a 404' do
+ it 'returns a 404', :aggregate_failures do
delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
expect(response).to have_gitlab_http_status(:not_found)
@@ -1059,7 +1136,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
project.add_developer(developer)
end
- it 'returns a 403' do
+ it 'returns a 403', :aggregate_failures do
delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", developer)
expect(response).to have_gitlab_http_status(:forbidden)
@@ -1078,7 +1155,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let_it_be(:build) { create(:ci_build, :failed, pipeline: pipeline) }
- it 'retries failed builds' do
+ it 'retries failed builds', :aggregate_failures do
expect do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user)
end.to change { pipeline.builds.count }.from(1).to(2)
@@ -1089,7 +1166,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'unauthorized user' do
- it 'does not return a project pipeline' do
+ it 'does not return a project pipeline', :aggregate_failures do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member)
expect(response).to have_gitlab_http_status(:not_found)
@@ -1106,7 +1183,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
end
- it 'returns error' do
+ it 'returns error', :aggregate_failures do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user)
expect(response).to have_gitlab_http_status(:forbidden)
@@ -1124,7 +1201,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let_it_be(:build) { create(:ci_build, :running, pipeline: pipeline) }
- context 'authorized user' do
+ context 'authorized user', :aggregate_failures do
it 'retries failed builds', :sidekiq_might_not_need_inline do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user)
@@ -1140,7 +1217,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
project.add_reporter(reporter)
end
- it 'rejects the action' do
+ it 'rejects the action', :aggregate_failures do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter)
expect(response).to have_gitlab_http_status(:forbidden)
@@ -1156,7 +1233,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when pipeline does not have a test report' do
- it 'returns an empty test report' do
+ it 'returns an empty test report', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -1167,7 +1244,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'when pipeline has a test report' do
let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
- it 'returns the test report' do
+ it 'returns the test report', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -1180,7 +1257,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
create(:ci_build, :broken_test_reports, name: 'rspec', pipeline: pipeline)
end
- it 'returns a suite_error' do
+ it 'returns a suite_error', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -1190,7 +1267,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'unauthorized user' do
- it 'does not return project pipelines' do
+ it 'does not return project pipelines', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/test_report", non_member)
expect(response).to have_gitlab_http_status(:not_found)
@@ -1208,7 +1285,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when pipeline does not have a test report summary' do
- it 'returns an empty test report summary' do
+ it 'returns an empty test report summary', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -1219,7 +1296,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
context 'when pipeline has a test report summary' do
let(:pipeline) { create(:ci_pipeline, :with_report_results, project: project) }
- it 'returns the test report summary' do
+ it 'returns the test report summary', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -1229,7 +1306,7 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do
end
context 'unauthorized user' do
- it 'does not return project pipelines' do
+ it 'does not return project pipelines', :aggregate_failures do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/test_report_summary", non_member)
expect(response).to have_gitlab_http_status(:not_found)
diff --git a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
index 3d3d699542b..596af1110cc 100644
--- a/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_artifacts_spec.rb
@@ -174,8 +174,21 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
expect(json_response['RemoteObject']).to have_key('StoreURL')
expect(json_response['RemoteObject']).to have_key('DeleteURL')
expect(json_response['RemoteObject']).to have_key('MultipartUpload')
+ expect(json_response['RemoteObject']['SkipDelete']).to eq(true)
expect(json_response['MaximumSize']).not_to be_nil
end
+
+ context 'when ci_artifacts_upload_to_final_location flag is disabled' do
+ before do
+ stub_feature_flags(ci_artifacts_upload_to_final_location: false)
+ end
+
+ it 'does not skip delete' do
+ subject
+
+ expect(json_response['RemoteObject']['SkipDelete']).to eq(false)
+ end
+ end
end
context 'when direct upload is disabled' do
@@ -255,8 +268,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
it 'tracks code_intelligence usage ping' do
tracking_params = {
event_names: 'i_source_code_code_intelligence',
- start_date: Date.yesterday,
- end_date: Date.today
+ start_date: Date.today.beginning_of_week,
+ end_date: 1.week.from_now
}
expect { authorize_artifacts_with_token_in_headers(artifact_type: :lsif) }
@@ -374,29 +387,53 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
let(:object) do
fog_connection.directories.new(key: 'artifacts').files.create( # rubocop:disable Rails/SaveBang
- key: 'tmp/uploads/12312300',
+ key: remote_path,
body: 'content'
)
end
let(:file_upload) { fog_to_uploaded_file(object) }
- before do
- upload_artifacts(file_upload, headers_with_token, 'file.remote_id' => remote_id)
- end
+ context 'when uploaded file has matching pending remote upload to its final location' do
+ let(:remote_path) { '12345/foo-bar-123' }
+ let(:object_remote_id) { remote_path }
+ let(:remote_id) { remote_path }
+
+ before do
+ allow(JobArtifactUploader).to receive(:generate_final_store_path).and_return(remote_path)
- context 'when valid remote_id is used' do
- let(:remote_id) { '12312300' }
+ ObjectStorage::PendingDirectUpload.prepare(
+ JobArtifactUploader.storage_location_identifier,
+ remote_path
+ )
+
+ upload_artifacts(file_upload, headers_with_token, 'file.remote_id' => remote_path)
+ end
it_behaves_like 'successful artifacts upload'
end
- context 'when invalid remote_id is used' do
- let(:remote_id) { 'invalid id' }
+ context 'when uploaded file is uploaded to temporary location' do
+ let(:object_remote_id) { JobArtifactUploader.generate_remote_id }
+ let(:remote_path) { File.join(ObjectStorage::TMP_UPLOAD_PATH, object_remote_id) }
+
+ before do
+ upload_artifacts(file_upload, headers_with_token, 'file.remote_id' => remote_id)
+ end
+
+ context 'and matching temporary remote_id is used' do
+ let(:remote_id) { object_remote_id }
+
+ it_behaves_like 'successful artifacts upload'
+ end
+
+ context 'and invalid remote_id is used' do
+ let(:remote_id) { JobArtifactUploader.generate_remote_id }
- it 'responds with bad request' do
- expect(response).to have_gitlab_http_status(:internal_server_error)
- expect(json_response['message']).to eq("Missing file")
+ it 'responds with internal server error' do
+ expect(response).to have_gitlab_http_status(:internal_server_error)
+ expect(json_response['message']).to eq("Missing file")
+ end
end
end
end
diff --git a/spec/requests/api/ci/runner/jobs_put_spec.rb b/spec/requests/api/ci/runner/jobs_put_spec.rb
index ef3b38e3fc4..ab7ab4e74f8 100644
--- a/spec/requests/api/ci/runner/jobs_put_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_put_spec.rb
@@ -21,13 +21,13 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
let_it_be(:project) { create(:project, namespace: group, shared_runners_enabled: false) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) }
- let_it_be(:runner_machine) { create(:ci_runner_machine, runner: runner) }
+ let_it_be(:runner_manager) { create(:ci_runner_machine, runner: runner) }
let_it_be(:user) { create(:user) }
describe 'PUT /api/v4/jobs/:id' do
let_it_be_with_reload(:job) do
create(:ci_build, :pending, :trace_live, pipeline: pipeline, project: project, user: user,
- runner_id: runner.id, runner_machine: runner_machine)
+ runner_id: runner.id, runner_manager: runner_manager)
end
before do
@@ -40,7 +40,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
it 'updates runner info' do
expect { update_job(state: 'success') }.to change { runner.reload.contacted_at }
- .and change { runner_machine.reload.contacted_at }
+ .and change { runner_manager.reload.contacted_at }
end
context 'when status is given' do
diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
index 6e721d40560..0164eda7680 100644
--- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb
@@ -122,56 +122,33 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
context 'when system_id parameter is specified' do
subject(:request) { request_job(**args) }
- context 'with create_runner_machine FF enabled' do
- before do
- stub_feature_flags(create_runner_machine: true)
- end
-
- context 'when ci_runner_machines with same system_xid does not exist' do
- let(:args) { { system_id: 's_some_system_id' } }
-
- it 'creates respective ci_runner_machines record', :freeze_time do
- expect { request }.to change { runner.runner_machines.reload.count }.from(0).to(1)
-
- machine = runner.runner_machines.last
- expect(machine.system_xid).to eq args[:system_id]
- expect(machine.runner).to eq runner
- expect(machine.contacted_at).to eq Time.current
- end
- end
-
- context 'when ci_runner_machines with same system_xid already exists', :freeze_time do
- let(:args) { { system_id: 's_existing_system_id' } }
- let!(:runner_machine) do
- create(:ci_runner_machine, runner: runner, system_xid: args[:system_id], contacted_at: 1.hour.ago)
- end
-
- it 'does not create new ci_runner_machines record' do
- expect { request }.not_to change { Ci::RunnerMachine.count }
- end
+ context 'when ci_runner_machines with same system_xid does not exist' do
+ let(:args) { { system_id: 's_some_system_id' } }
- it 'updates the contacted_at field' do
- request
+ it 'creates respective ci_runner_machines record', :freeze_time do
+ expect { request }.to change { runner.runner_managers.reload.count }.from(0).to(1)
- expect(runner_machine.reload.contacted_at).to eq Time.current
- end
+ runner_manager = runner.runner_managers.last
+ expect(runner_manager.system_xid).to eq args[:system_id]
+ expect(runner_manager.runner).to eq runner
+ expect(runner_manager.contacted_at).to eq Time.current
end
end
- context 'with create_runner_machine FF disabled' do
- before do
- stub_feature_flags(create_runner_machine: false)
+ context 'when ci_runner_machines with same system_xid already exists', :freeze_time do
+ let(:args) { { system_id: 's_existing_system_id' } }
+ let!(:runner_manager) do
+ create(:ci_runner_machine, runner: runner, system_xid: args[:system_id], contacted_at: 1.hour.ago)
end
- context 'when ci_runner_machines with same system_xid does not exist' do
- let(:args) { { system_id: 's_some_system_id' } }
+ it 'does not create new ci_runner_machines record' do
+ expect { request }.not_to change { Ci::RunnerManager.count }
+ end
- it 'does not create respective ci_runner_machines record', :freeze_time, :aggregate_failures do
- expect { request }.not_to change { runner.runner_machines.reload.count }
+ it 'updates the contacted_at field' do
+ request
- expect(response).to have_gitlab_http_status(:created)
- expect(runner.runner_machines).to be_empty
- end
+ expect(runner_manager.reload.contacted_at).to eq Time.current
end
end
end
@@ -253,11 +230,14 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
end
let(:expected_cache) do
- [{ 'key' => a_string_matching(/^cache_key-(?>protected|non_protected)$/),
- 'untracked' => false,
- 'paths' => ['vendor/*'],
- 'policy' => 'pull-push',
- 'when' => 'on_success' }]
+ [{
+ 'key' => a_string_matching(/^cache_key-(?>protected|non_protected)$/),
+ 'untracked' => false,
+ 'paths' => ['vendor/*'],
+ 'policy' => 'pull-push',
+ 'when' => 'on_success',
+ 'fallback_keys' => []
+ }]
end
let(:expected_features) do
@@ -366,36 +346,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
end
end
- context 'when job filtered by job_age' do
- let!(:job) do
- create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, queued_at: 60.seconds.ago)
- end
-
- before do
- job.queuing_entry&.update!(created_at: 60.seconds.ago)
- end
-
- context 'job is queued less than job_age parameter' do
- let(:job_age) { 120 }
-
- it 'gives 204' do
- request_job(job_age: job_age)
-
- expect(response).to have_gitlab_http_status(:no_content)
- end
- end
-
- context 'job is queued more than job_age parameter' do
- let(:job_age) { 30 }
-
- it 'picks a job' do
- request_job(job_age: job_age)
-
- expect(response).to have_gitlab_http_status(:created)
- end
- end
- end
-
context 'when job is made for branch' do
it 'sets tag as ref_type' do
request_job
@@ -831,19 +781,6 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
end
end
end
-
- context 'when the FF ci_hooks_pre_get_sources_script is disabled' do
- before do
- stub_feature_flags(ci_hooks_pre_get_sources_script: false)
- end
-
- it 'does not return the pre_get_sources_script' do
- request_job
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response).not_to have_key('hooks')
- end
- end
end
describe 'port support' do
diff --git a/spec/requests/api/ci/runner/runners_delete_spec.rb b/spec/requests/api/ci/runner/runners_delete_spec.rb
index 65c287a9535..681dd4d701e 100644
--- a/spec/requests/api/ci/runner/runners_delete_spec.rb
+++ b/spec/requests/api/ci/runner/runners_delete_spec.rb
@@ -7,16 +7,19 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
include RedisHelpers
include WorkhorseHelpers
- let(:registration_token) { 'abcdefg123456' }
-
before do
stub_feature_flags(ci_enable_live_trace: true)
stub_gitlab_calls
- stub_application_setting(runners_registration_token: registration_token)
- allow_any_instance_of(::Ci::Runner).to receive(:cache_attributes)
+ allow_next_instance_of(::Ci::Runner) { |runner| allow(runner).to receive(:cache_attributes) }
end
describe '/api/v4/runners' do
+ let(:registration_token) { 'abcdefg123456' }
+
+ before do
+ stub_application_setting(runners_registration_token: registration_token)
+ end
+
describe 'DELETE /api/v4/runners' do
context 'when no token is provided' do
it 'returns 400 error' do
@@ -57,4 +60,85 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
end
end
end
+
+ describe '/api/v4/runners/managers' do
+ describe 'DELETE /api/v4/runners/managers' do
+ subject(:delete_request) { delete api('/runners/managers'), params: delete_params }
+
+ context 'with created runner' do
+ let!(:runner) { create(:ci_runner, :with_runner_manager, registration_type: :authenticated_user) }
+
+ context 'with matching system_id' do
+ context 'when no token is provided' do
+ let(:delete_params) { { system_id: runner.runner_managers.first.system_xid } }
+
+ it 'returns 400 error' do
+ delete_request
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'when invalid token is provided' do
+ let(:delete_params) { { token: 'invalid', system_id: runner.runner_managers.first.system_xid } }
+
+ it 'returns 403 error' do
+ delete_request
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+ end
+
+ context 'when valid token is provided' do
+ context 'with created runner' do
+ let!(:runner) { create(:ci_runner, :with_runner_manager, registration_type: :authenticated_user) }
+
+ context 'with matching system_id' do
+ let(:delete_params) { { token: runner.token, system_id: runner.runner_managers.first.system_xid } }
+
+ it 'deletes runner manager' do
+ expect do
+ delete_request
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end.to change { runner.runner_managers.count }.from(1).to(0)
+
+ expect(::Ci::Runner.count).to eq(1)
+ end
+
+ it_behaves_like '412 response' do
+ let(:request) { api('/runners/managers') }
+ let(:params) { delete_params }
+ end
+
+ it_behaves_like 'storing arguments in the application context for the API' do
+ let(:expected_params) { { client_id: "runner/#{runner.id}" } }
+ end
+ end
+
+ context 'with unknown system_id' do
+ let(:delete_params) { { token: runner.token, system_id: 'unknown_system_id' } }
+
+ it 'returns 404 error' do
+ delete_request
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'without system_id' do
+ let(:delete_params) { { token: runner.token } }
+
+ it 'does not delete runner manager nor runner' do
+ delete_request
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/requests/api/ci/runner/runners_post_spec.rb b/spec/requests/api/ci/runner/runners_post_spec.rb
index 73f8e87a9fb..a36ea2115cf 100644
--- a/spec/requests/api/ci/runner/runners_post_spec.rb
+++ b/spec/requests/api/ci/runner/runners_post_spec.rb
@@ -15,14 +15,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
context 'when invalid token is provided' do
it 'returns 403 error' do
- allow_next_instance_of(::Ci::Runners::RegisterRunnerService) do |service|
- allow(service).to receive(:execute)
- .and_return(ServiceResponse.error(message: 'invalid token supplied', http_status: :forbidden))
- end
-
post api('/runners'), params: { token: 'invalid' }
expect(response).to have_gitlab_http_status(:forbidden)
+ expect(json_response['message']).to eq('403 Forbidden - invalid token supplied')
end
end
@@ -44,21 +40,24 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
let_it_be(:new_runner) { create(:ci_runner) }
before do
- allow_next_instance_of(::Ci::Runners::RegisterRunnerService) do |service|
- expected_params = {
- description: 'server.hostname',
- maintenance_note: 'Some maintainer notes',
- run_untagged: false,
- tag_list: %w(tag1 tag2),
- locked: true,
- active: true,
- access_level: 'ref_protected',
- maximum_timeout: 9000
- }.stringify_keys
+ expected_params = {
+ description: 'server.hostname',
+ maintenance_note: 'Some maintainer notes',
+ run_untagged: false,
+ tag_list: %w(tag1 tag2),
+ locked: true,
+ active: true,
+ access_level: 'ref_protected',
+ maximum_timeout: 9000
+ }.stringify_keys
+ allow_next_instance_of(
+ ::Ci::Runners::RegisterRunnerService,
+ 'valid token',
+ a_hash_including(expected_params)
+ ) do |service|
expect(service).to receive(:execute)
.once
- .with('valid token', a_hash_including(expected_params))
.and_return(ServiceResponse.success(payload: { runner: new_runner }))
end
end
@@ -109,11 +108,14 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
let(:new_runner) { create(:ci_runner) }
it 'converts to maintenance_note param' do
- allow_next_instance_of(::Ci::Runners::RegisterRunnerService) do |service|
+ allow_next_instance_of(
+ ::Ci::Runners::RegisterRunnerService,
+ 'valid token',
+ a_hash_including('maintenance_note' => 'Some maintainer notes')
+ .and(excluding('maintainter_note' => anything))
+ ) do |service|
expect(service).to receive(:execute)
.once
- .with('valid token', a_hash_including('maintenance_note' => 'Some maintainer notes')
- .and(excluding('maintainter_note' => anything)))
.and_return(ServiceResponse.success(payload: { runner: new_runner }))
end
@@ -134,12 +136,13 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
let_it_be(:new_runner) { build(:ci_runner) }
it 'uses active value in registration' do
- expect_next_instance_of(::Ci::Runners::RegisterRunnerService) do |service|
- expected_params = { active: false }.stringify_keys
-
+ expect_next_instance_of(
+ ::Ci::Runners::RegisterRunnerService,
+ 'valid token',
+ a_hash_including({ active: false }.stringify_keys)
+ ) do |service|
expect(service).to receive(:execute)
.once
- .with('valid token', a_hash_including(expected_params))
.and_return(ServiceResponse.success(payload: { runner: new_runner }))
end
@@ -197,12 +200,13 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
let(:tag_list) { (1..::Ci::Runner::TAG_LIST_MAX_LENGTH + 1).map { |i| "tag#{i}" } }
it 'uses tag_list value in registration and returns error' do
- expect_next_instance_of(::Ci::Runners::RegisterRunnerService) do |service|
- expected_params = { tag_list: tag_list }.stringify_keys
-
+ expect_next_instance_of(
+ ::Ci::Runners::RegisterRunnerService,
+ registration_token,
+ a_hash_including({ tag_list: tag_list }.stringify_keys)
+ ) do |service|
expect(service).to receive(:execute)
.once
- .with(registration_token, a_hash_including(expected_params))
.and_call_original
end
@@ -217,12 +221,13 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
let(:tag_list) { (1..20).map { |i| "tag#{i}" } }
it 'uses tag_list value in registration and successfully creates runner' do
- expect_next_instance_of(::Ci::Runners::RegisterRunnerService) do |service|
- expected_params = { tag_list: tag_list }.stringify_keys
-
+ expect_next_instance_of(
+ ::Ci::Runners::RegisterRunnerService,
+ registration_token,
+ a_hash_including({ tag_list: tag_list }.stringify_keys)
+ ) do |service|
expect(service).to receive(:execute)
.once
- .with(registration_token, a_hash_including(expected_params))
.and_call_original
end
@@ -232,6 +237,18 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
end
end
end
+
+ context 'when runner registration is disallowed' do
+ before do
+ stub_application_setting(allow_runner_registration_token: false)
+ end
+
+ it 'returns 410 Gone status' do
+ post api('/runners'), params: { token: registration_token }
+
+ expect(response).to have_gitlab_http_status(:gone)
+ end
+ end
end
end
end
diff --git a/spec/requests/api/ci/runner/runners_verify_post_spec.rb b/spec/requests/api/ci/runner/runners_verify_post_spec.rb
index a6a1ad947aa..f1b33826f5e 100644
--- a/spec/requests/api/ci/runner/runners_verify_post_spec.rb
+++ b/spec/requests/api/ci/runner/runners_verify_post_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
end
describe '/api/v4/runners' do
- describe 'POST /api/v4/runners/verify' do
+ describe 'POST /api/v4/runners/verify', :freeze_time do
let_it_be_with_reload(:runner) { create(:ci_runner, token_expires_at: 3.days.from_now) }
let(:params) {}
@@ -45,9 +45,12 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
context 'when valid token is provided' do
let(:params) { { token: runner.token } }
- context 'with create_runner_machine FF enabled' do
- before do
- stub_feature_flags(create_runner_machine: true)
+ context 'with glrt-prefixed token' do
+ let_it_be(:registration_token) { 'glrt-abcdefg123456' }
+ let_it_be(:registration_type) { :authenticated_user }
+ let_it_be(:runner) do
+ create(:ci_runner, registration_type: registration_type,
+ token: registration_token, token_expires_at: 3.days.from_now)
end
it 'verifies Runner credentials' do
@@ -61,39 +64,29 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
})
end
- context 'with non-expiring runner token' do
- before do
- runner.update!(token_expires_at: nil)
- end
-
- it 'verifies Runner credentials' do
- verify
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq({
- 'id' => runner.id,
- 'token' => runner.token,
- 'token_expires_at' => nil
- })
- end
+ it 'does not update contacted_at' do
+ expect { verify }.not_to change { runner.reload.contacted_at }.from(nil)
end
+ end
- it_behaves_like 'storing arguments in the application context for the API' do
- let(:expected_params) { { client_id: "runner/#{runner.id}" } }
- end
+ it 'verifies Runner credentials' do
+ verify
- context 'when system_id is provided' do
- let(:params) { { token: runner.token, system_id: 's_some_system_id' } }
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq({
+ 'id' => runner.id,
+ 'token' => runner.token,
+ 'token_expires_at' => runner.token_expires_at.iso8601(3)
+ })
+ end
- it 'creates a runner_machine' do
- expect { verify }.to change { Ci::RunnerMachine.count }.by(1)
- end
- end
+ it 'updates contacted_at' do
+ expect { verify }.to change { runner.reload.contacted_at }.from(nil).to(Time.current)
end
- context 'with create_runner_machine FF disabled' do
+ context 'with non-expiring runner token' do
before do
- stub_feature_flags(create_runner_machine: false)
+ runner.update!(token_expires_at: nil)
end
it 'verifies Runner credentials' do
@@ -103,18 +96,20 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
expect(json_response).to eq({
'id' => runner.id,
'token' => runner.token,
- 'token_expires_at' => runner.token_expires_at.iso8601(3)
+ 'token_expires_at' => nil
})
end
+ end
- context 'when system_id is provided' do
- let(:params) { { token: runner.token, system_id: 's_some_system_id' } }
+ it_behaves_like 'storing arguments in the application context for the API' do
+ let(:expected_params) { { client_id: "runner/#{runner.id}" } }
+ end
- it 'does not create a runner_machine', :aggregate_failures do
- expect { verify }.not_to change { Ci::RunnerMachine.count }
+ context 'when system_id is provided' do
+ let(:params) { { token: runner.token, system_id: 's_some_system_id' } }
- expect(response).to have_gitlab_http_status(:ok)
- end
+ it 'creates a runner_manager' do
+ expect { verify }.to change { Ci::RunnerManager.count }.by(1)
end
end
end
diff --git a/spec/requests/api/ci/runners_reset_registration_token_spec.rb b/spec/requests/api/ci/runners_reset_registration_token_spec.rb
index 1110dbf5fbc..98edde93e95 100644
--- a/spec/requests/api/ci/runners_reset_registration_token_spec.rb
+++ b/spec/requests/api/ci/runners_reset_registration_token_spec.rb
@@ -3,10 +3,12 @@
require 'spec_helper'
RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
- subject { post api("#{prefix}/runners/reset_registration_token", user) }
+ let_it_be(:admin_mode) { false }
+
+ subject { post api("#{prefix}/runners/reset_registration_token", user, admin_mode: admin_mode) }
shared_examples 'bad request' do |result|
- it 'returns 400 error' do
+ it 'returns 400 error', :aggregate_failures do
expect { subject }.not_to change { get_token }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -15,7 +17,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
shared_examples 'unauthenticated' do
- it 'returns 401 error' do
+ it 'returns 401 error', :aggregate_failures do
expect { subject }.not_to change { get_token }
expect(response).to have_gitlab_http_status(:unauthorized)
@@ -23,7 +25,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
shared_examples 'unauthorized' do
- it 'returns 403 error' do
+ it 'returns 403 error', :aggregate_failures do
expect { subject }.not_to change { get_token }
expect(response).to have_gitlab_http_status(:forbidden)
@@ -31,7 +33,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
shared_examples 'not found' do |scope|
- it 'returns 404 error' do
+ it 'returns 404 error', :aggregate_failures do
expect { subject }.not_to change { get_token }
expect(response).to have_gitlab_http_status(:not_found)
@@ -58,7 +60,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
shared_context 'when authorized' do |scope|
- it 'resets runner registration token' do
+ it 'resets runner registration token', :aggregate_failures do
expect { subject }.to change { get_token }
expect(response).to have_gitlab_http_status(:success)
@@ -99,6 +101,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
include_context 'when authorized', 'instance' do
let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:admin_mode) { true }
def get_token
ApplicationSetting.current_without_cache.runners_registration_token
diff --git a/spec/requests/api/ci/runners_spec.rb b/spec/requests/api/ci/runners_spec.rb
index ca051386265..2b2d2e0def8 100644
--- a/spec/requests/api/ci/runners_spec.rb
+++ b/spec/requests/api/ci/runners_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
+RSpec.describe API::Ci::Runners, :aggregate_failures, feature_category: :runner_fleet do
let_it_be(:admin) { create(:user, :admin) }
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
@@ -134,17 +134,21 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
describe 'GET /runners/all' do
+ let(:path) { '/runners/all' }
+
+ it_behaves_like 'GET request permissions for admin mode'
+
context 'authorized user' do
context 'with admin privileges' do
it 'returns response status and headers' do
- get api('/runners/all', admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
end
it 'returns all runners' do
- get api('/runners/all', admin)
+ get api(path, admin, admin_mode: true)
expect(json_response).to match_array [
a_hash_including('description' => 'Project runner', 'is_shared' => false, 'active' => true, 'paused' => false, 'runner_type' => 'project_type'),
@@ -156,7 +160,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it 'filters runners by scope' do
- get api('/runners/all?scope=shared', admin)
+ get api('/runners/all?scope=shared', admin, admin_mode: true)
shared = json_response.all? { |r| r['is_shared'] }
expect(response).to have_gitlab_http_status(:ok)
@@ -167,7 +171,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it 'filters runners by scope' do
- get api('/runners/all?scope=specific', admin)
+ get api('/runners/all?scope=specific', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -181,12 +185,12 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it 'avoids filtering if scope is invalid' do
- get api('/runners/all?scope=unknown', admin)
+ get api('/runners/all?scope=unknown', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'filters runners by project type' do
- get api('/runners/all?type=project_type', admin)
+ get api('/runners/all?type=project_type', admin, admin_mode: true)
expect(json_response).to match_array [
a_hash_including('description' => 'Project runner'),
@@ -195,7 +199,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it 'filters runners by group type' do
- get api('/runners/all?type=group_type', admin)
+ get api('/runners/all?type=group_type', admin, admin_mode: true)
expect(json_response).to match_array [
a_hash_including('description' => 'Group runner A'),
@@ -204,7 +208,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it 'does not filter by invalid type' do
- get api('/runners/all?type=bogus', admin)
+ get api('/runners/all?type=bogus', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -213,7 +217,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
let_it_be(:runner) { create(:ci_runner, :project, :inactive, description: 'Inactive project runner', projects: [project]) }
it 'filters runners by status' do
- get api('/runners/all?paused=true', admin)
+ get api('/runners/all?paused=true', admin, admin_mode: true)
expect(json_response).to match_array [
a_hash_including('description' => 'Inactive project runner')
@@ -221,7 +225,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it 'filters runners by status' do
- get api('/runners/all?status=paused', admin)
+ get api('/runners/all?status=paused', admin, admin_mode: true)
expect(json_response).to match_array [
a_hash_including('description' => 'Inactive project runner')
@@ -230,7 +234,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it 'does not filter by invalid status' do
- get api('/runners/all?status=bogus', admin)
+ get api('/runners/all?status=bogus', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -239,7 +243,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
create(:ci_runner, :project, description: 'Runner tagged with tag1 and tag2', projects: [project], tag_list: %w[tag1 tag2])
create(:ci_runner, :project, description: 'Runner tagged with tag2', projects: [project], tag_list: ['tag2'])
- get api('/runners/all?tag_list=tag1,tag2', admin)
+ get api('/runners/all?tag_list=tag1,tag2', admin, admin_mode: true)
expect(json_response).to match_array [
a_hash_including('description' => 'Runner tagged with tag1 and tag2')
@@ -249,7 +253,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'without admin privileges' do
it 'does not return runners list' do
- get api('/runners/all', user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -266,6 +270,10 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
describe 'GET /runners/:id' do
+ let(:path) { "/runners/#{project_runner.id}" }
+
+ it_behaves_like 'GET request permissions for admin mode'
+
context 'admin user' do
context 'when runner is shared' do
it "returns runner's details" do
@@ -286,7 +294,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
it 'deletes unused runner' do
expect do
- delete api("/runners/#{unused_project_runner.id}", admin)
+ delete api("/runners/#{unused_project_runner.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
end.to change { ::Ci::Runner.project_type.count }.by(-1)
@@ -294,21 +302,21 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it "returns runner's details" do
- get api("/runners/#{project_runner.id}", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['description']).to eq(project_runner.description)
end
it "returns the project's details for a project runner" do
- get api("/runners/#{project_runner.id}", admin)
+ get api(path, admin, admin_mode: true)
expect(json_response['projects'].first['id']).to eq(project.id)
end
end
it 'returns 404 if runner does not exist' do
- get api('/runners/0', admin)
+ get api("/runners/#{non_existing_record_id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -316,7 +324,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'when the runner is a group runner' do
it "returns the runner's details" do
- get api("/runners/#{group_runner_a.id}", admin)
+ get api("/runners/#{group_runner_a.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['description']).to eq(group_runner_a.description)
@@ -327,7 +335,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context "runner project's administrative user" do
context 'when runner is not shared' do
it "returns runner's details" do
- get api("/runners/#{project_runner.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['description']).to eq(project_runner.description)
@@ -346,7 +354,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'other authorized user' do
it "does not return project runner's details" do
- get api("/runners/#{project_runner.id}", user2)
+ get api(path, user2)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -354,7 +362,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'unauthorized user' do
it "does not return project runner's details" do
- get api("/runners/#{project_runner.id}")
+ get api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -362,6 +370,12 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
describe 'PUT /runners/:id' do
+ let(:path) { "/runners/#{project_runner.id}" }
+
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:params) { { description: 'test' } }
+ end
+
context 'admin user' do
# see https://gitlab.com/gitlab-org/gitlab-foss/issues/48625
context 'single parameter update' do
@@ -492,20 +506,22 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it 'returns 404 if runner does not exist' do
- update_runner(0, admin, description: 'test')
+ update_runner(non_existing_record_id, admin, description: 'test')
expect(response).to have_gitlab_http_status(:not_found)
end
def update_runner(id, user, args)
- put api("/runners/#{id}", user), params: args
+ put api("/runners/#{id}", user, admin_mode: true), params: args
end
end
context 'authorized user' do
+ let_it_be(:params) { { description: 'test' } }
+
context 'when runner is shared' do
it 'does not update runner' do
- put api("/runners/#{shared_runner.id}", user), params: { description: 'test' }
+ put api("/runners/#{shared_runner.id}", user), params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -513,17 +529,16 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'when runner is not shared' do
it 'does not update project runner without access to it' do
- put api("/runners/#{project_runner.id}", user2), params: { description: 'test' }
+ put api(path, user2), params: { description: 'test' }
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'updates project runner with access to it' do
description = project_runner.description
- put api("/runners/#{project_runner.id}", admin), params: { description: 'test' }
+ put api(path, admin, admin_mode: true), params: params
project_runner.reload
- expect(response).to have_gitlab_http_status(:ok)
expect(project_runner.description).to eq('test')
expect(project_runner.description).not_to eq(description)
end
@@ -532,7 +547,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'unauthorized user' do
it 'does not delete project runner' do
- put api("/runners/#{project_runner.id}")
+ put api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -540,6 +555,10 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
describe 'DELETE /runners/:id' do
+ let(:path) { "/runners/#{shared_runner.id}" }
+
+ it_behaves_like 'DELETE request permissions for admin mode'
+
context 'admin user' do
context 'when runner is shared' do
it 'deletes runner' do
@@ -548,14 +567,14 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
expect do
- delete api("/runners/#{shared_runner.id}", admin)
+ delete api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
end.to change { ::Ci::Runner.instance_type.count }.by(-1)
end
it_behaves_like '412 response' do
- let(:request) { api("/runners/#{shared_runner.id}", admin) }
+ let(:request) { api(path, admin, admin_mode: true) }
end
end
@@ -566,7 +585,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
expect do
- delete api("/runners/#{project_runner.id}", admin)
+ delete api("/runners/#{project_runner.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
end.to change { ::Ci::Runner.project_type.count }.by(-1)
@@ -578,7 +597,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
expect(service).not_to receive(:execute)
end
- delete api('/runners/0', admin)
+ delete api("/runners/#{non_existing_record_id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -587,7 +606,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'authorized user' do
context 'when runner is shared' do
it 'does not delete runner' do
- delete api("/runners/#{shared_runner.id}", user)
+ delete api(path, user)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
@@ -671,10 +690,16 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
describe 'POST /runners/:id/reset_authentication_token' do
+ let(:path) { "/runners/#{shared_runner.id}/reset_authentication_token" }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { {} }
+ end
+
context 'admin user' do
it 'resets shared runner authentication token' do
expect do
- post api("/runners/#{shared_runner.id}/reset_authentication_token", admin)
+ post api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:success)
expect(json_response).to eq({ 'token' => shared_runner.reload.token, 'token_expires_at' => nil })
@@ -682,7 +707,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it 'returns 404 if runner does not exist' do
- post api('/runners/0/reset_authentication_token', admin)
+ post api("/runners/#{non_existing_record_id}/reset_authentication_token", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -765,7 +790,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'unauthorized user' do
it 'does not reset authentication token' do
expect do
- post api("/runners/#{shared_runner.id}/reset_authentication_token")
+ post api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end.not_to change { shared_runner.reload.token }
@@ -779,12 +804,15 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
let_it_be(:job_3) { create(:ci_build, :failed, runner: shared_runner, project: project) }
let_it_be(:job_4) { create(:ci_build, :running, runner: project_runner, project: project) }
let_it_be(:job_5) { create(:ci_build, :failed, runner: project_runner, project: project) }
+ let(:path) { "/runners/#{project_runner.id}/jobs" }
+
+ it_behaves_like 'GET request permissions for admin mode'
context 'admin user' do
context 'when runner exists' do
context 'when runner is shared' do
it 'return jobs' do
- get api("/runners/#{shared_runner.id}/jobs", admin)
+ get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -796,7 +824,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'when runner is a project runner' do
it 'return jobs' do
- get api("/runners/#{project_runner.id}/jobs", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -825,7 +853,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'when valid status is provided' do
it 'return filtered jobs' do
- get api("/runners/#{project_runner.id}/jobs?status=failed", admin)
+ get api("/runners/#{project_runner.id}/jobs?status=failed", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -839,7 +867,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'when valid order_by is provided' do
context 'when sort order is not specified' do
it 'return jobs in descending order' do
- get api("/runners/#{project_runner.id}/jobs?order_by=id", admin)
+ get api("/runners/#{project_runner.id}/jobs?order_by=id", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -852,7 +880,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'when sort order is specified as asc' do
it 'return jobs sorted in ascending order' do
- get api("/runners/#{project_runner.id}/jobs?order_by=id&sort=asc", admin)
+ get api("/runners/#{project_runner.id}/jobs?order_by=id&sort=asc", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -866,7 +894,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'when invalid status is provided' do
it 'return 400' do
- get api("/runners/#{project_runner.id}/jobs?status=non-existing", admin)
+ get api("/runners/#{project_runner.id}/jobs?status=non-existing", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -874,7 +902,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'when invalid order_by is provided' do
it 'return 400' do
- get api("/runners/#{project_runner.id}/jobs?order_by=non-existing", admin)
+ get api("/runners/#{project_runner.id}/jobs?order_by=non-existing", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -882,7 +910,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'when invalid sort is provided' do
it 'return 400' do
- get api("/runners/#{project_runner.id}/jobs?sort=non-existing", admin)
+ get api("/runners/#{project_runner.id}/jobs?sort=non-existing", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -890,16 +918,16 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it 'avoids N+1 DB queries' do
- get api("/runners/#{shared_runner.id}/jobs", admin)
+ get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true)
control = ActiveRecord::QueryRecorder.new do
- get api("/runners/#{shared_runner.id}/jobs", admin)
+ get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true)
end
create(:ci_build, :failed, runner: shared_runner, project: project)
expect do
- get api("/runners/#{shared_runner.id}/jobs", admin)
+ get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true)
end.not_to exceed_query_limit(control.count)
end
@@ -925,12 +953,12 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
]).once.and_call_original
end
- get api("/runners/#{shared_runner.id}/jobs", admin), params: { per_page: 2, order_by: 'id', sort: 'desc' }
+ get api("/runners/#{shared_runner.id}/jobs", admin, admin_mode: true), params: { per_page: 2, order_by: 'id', sort: 'desc' }
end
context "when runner doesn't exist" do
it 'returns 404' do
- get api('/runners/0/jobs', admin)
+ get api('/runners/0/jobs', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -949,7 +977,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'when runner is a project runner' do
it 'return jobs' do
- get api("/runners/#{project_runner.id}/jobs", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -992,7 +1020,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'other authorized user' do
it 'does not return jobs' do
- get api("/runners/#{project_runner.id}/jobs", user2)
+ get api(path, user2)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -1000,7 +1028,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'unauthorized user' do
it 'does not return jobs' do
- get api("/runners/#{project_runner.id}/jobs")
+ get api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -1028,7 +1056,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
describe 'GET /projects/:id/runners' do
context 'authorized user with maintainer privileges' do
it 'returns response status and headers' do
- get api('/runners/all', admin)
+ get api('/runners/all', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -1200,19 +1228,27 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
describe 'POST /projects/:id/runners' do
+ let(:path) { "/projects/#{project.id}/runners" }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let!(:new_project_runner) { create(:ci_runner, :project) }
+ let(:params) { { runner_id: new_project_runner.id } }
+ let(:failed_status_code) { :not_found }
+ end
+
context 'authorized user' do
let_it_be(:project_runner2) { create(:ci_runner, :project, projects: [project2]) }
it 'enables project runner' do
expect do
- post api("/projects/#{project.id}/runners", user), params: { runner_id: project_runner2.id }
+ post api(path, user), params: { runner_id: project_runner2.id }
end.to change { project.runners.count }.by(+1)
expect(response).to have_gitlab_http_status(:created)
end
it 'avoids changes when enabling already enabled runner' do
expect do
- post api("/projects/#{project.id}/runners", user), params: { runner_id: project_runner.id }
+ post api(path, user), params: { runner_id: project_runner.id }
end.to change { project.runners.count }.by(0)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -1221,20 +1257,20 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
project_runner2.update!(locked: true)
expect do
- post api("/projects/#{project.id}/runners", user), params: { runner_id: project_runner2.id }
+ post api(path, user), params: { runner_id: project_runner2.id }
end.to change { project.runners.count }.by(0)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'does not enable shared runner' do
- post api("/projects/#{project.id}/runners", user), params: { runner_id: shared_runner.id }
+ post api(path, user), params: { runner_id: shared_runner.id }
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'does not enable group runner' do
- post api("/projects/#{project.id}/runners", user), params: { runner_id: group_runner_a.id }
+ post api(path, user), params: { runner_id: group_runner_a.id }
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -1245,7 +1281,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
it 'enables any project runner' do
expect do
- post api("/projects/#{project.id}/runners", admin), params: { runner_id: new_project_runner.id }
+ post api(path, admin, admin_mode: true), params: { runner_id: new_project_runner.id }
end.to change { project.runners.count }.by(+1)
expect(response).to have_gitlab_http_status(:created)
end
@@ -1257,7 +1293,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
it 'does not enable project runner' do
expect do
- post api("/projects/#{project.id}/runners", admin), params: { runner_id: new_project_runner.id }
+ post api(path, admin, admin_mode: true), params: { runner_id: new_project_runner.id }
end.not_to change { project.runners.count }
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -1266,7 +1302,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
end
it 'raises an error when no runner_id param is provided' do
- post api("/projects/#{project.id}/runners", admin)
+ post api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -1276,7 +1312,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
let!(:new_project_runner) { create(:ci_runner, :project) }
it 'does not enable runner without access to' do
- post api("/projects/#{project.id}/runners", user), params: { runner_id: new_project_runner.id }
+ post api(path, user), params: { runner_id: new_project_runner.id }
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -1284,7 +1320,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'authorized user without permissions' do
it 'does not enable runner' do
- post api("/projects/#{project.id}/runners", user2)
+ post api(path, user2)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -1292,7 +1328,7 @@ RSpec.describe API::Ci::Runners, feature_category: :runner_fleet do
context 'unauthorized user' do
it 'does not enable runner' do
- post api("/projects/#{project.id}/runners")
+ post api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
diff --git a/spec/requests/api/ci/secure_files_spec.rb b/spec/requests/api/ci/secure_files_spec.rb
index fc988800b56..db12576154e 100644
--- a/spec/requests/api/ci/secure_files_spec.rb
+++ b/spec/requests/api/ci/secure_files_spec.rb
@@ -136,7 +136,7 @@ RSpec.describe API::Ci::SecureFiles, feature_category: :mobile_devops do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(secure_file_with_metadata.name)
- expect(json_response['expires_at']).to eq('2022-04-26T19:20:40.000Z')
+ expect(json_response['expires_at']).to eq('2023-04-26T19:20:39.000Z')
expect(json_response['metadata'].keys).to match_array(%w[id issuer subject expires_at])
expect(json_response['file_extension']).to eq('cer')
end
diff --git a/spec/requests/api/ci/variables_spec.rb b/spec/requests/api/ci/variables_spec.rb
index 0f9f1bc80d6..e937c4c2b8f 100644
--- a/spec/requests/api/ci/variables_spec.rb
+++ b/spec/requests/api/ci/variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Ci::Variables, feature_category: :pipeline_authoring do
+RSpec.describe API::Ci::Variables, feature_category: :secrets_management do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) }
diff --git a/spec/requests/api/clusters/agent_tokens_spec.rb b/spec/requests/api/clusters/agent_tokens_spec.rb
index b2d996e8002..2647684c9f8 100644
--- a/spec/requests/api/clusters/agent_tokens_spec.rb
+++ b/spec/requests/api/clusters/agent_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Clusters::AgentTokens, feature_category: :kubernetes_management do
+RSpec.describe API::Clusters::AgentTokens, feature_category: :deployment_management do
let_it_be(:agent) { create(:cluster_agent) }
let_it_be(:agent_token_one) { create(:cluster_agent_token, agent: agent) }
let_it_be(:revoked_agent_token) { create(:cluster_agent_token, :revoked, agent: agent) }
@@ -17,18 +17,16 @@ RSpec.describe API::Clusters::AgentTokens, feature_category: :kubernetes_managem
describe 'GET /projects/:id/cluster_agents/:agent_id/tokens' do
context 'with authorized user' do
- it 'returns tokens regardless of status' do
+ it 'only returns active agent tokens' do
get api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens", user)
aggregate_failures "testing response" do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/agent_tokens')
- expect(json_response.count).to eq(2)
+ expect(json_response.count).to eq(1)
expect(json_response.first['name']).to eq(agent_token_one.name)
expect(json_response.first['agent_id']).to eq(agent.id)
- expect(json_response.second['name']).to eq(revoked_agent_token.name)
- expect(json_response.second['agent_id']).to eq(agent.id)
end
end
@@ -80,17 +78,10 @@ RSpec.describe API::Clusters::AgentTokens, feature_category: :kubernetes_managem
end
end
- it 'returns an agent token that is revoked' do
+ it 'returns a 404 if agent token is revoked' do
get api("/projects/#{project.id}/cluster_agents/#{agent.id}/tokens/#{revoked_agent_token.id}", user)
- aggregate_failures "testing response" do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/agent_token')
- expect(json_response['id']).to eq(revoked_agent_token.id)
- expect(json_response['name']).to eq(revoked_agent_token.name)
- expect(json_response['agent_id']).to eq(agent.id)
- expect(json_response['status']).to eq('revoked')
- end
+ expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns a 404 if agent does not exist' do
diff --git a/spec/requests/api/clusters/agents_spec.rb b/spec/requests/api/clusters/agents_spec.rb
index a09713bd6e7..12056567e9d 100644
--- a/spec/requests/api/clusters/agents_spec.rb
+++ b/spec/requests/api/clusters/agents_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Clusters::Agents, feature_category: :kubernetes_management do
+RSpec.describe API::Clusters::Agents, feature_category: :deployment_management do
let_it_be(:agent) { create(:cluster_agent) }
let(:user) { agent.created_by_user }
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 025d065df7b..7540e19e278 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -533,8 +533,8 @@ RSpec.describe API::CommitStatuses, feature_category: :continuous_integration do
end
end
- context 'with partitions' do
- let(:current_partition_id) { 123 }
+ context 'with partitions', :ci_partitionable do
+ let(:current_partition_id) { ci_testing_partition_id }
before do
allow(Ci::Pipeline)
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index bcc27a80cf8..28126f1bdc2 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -132,6 +132,42 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
it_behaves_like 'project commits'
end
+ context 'with author parameter' do
+ let(:params) { { author: 'Zaporozhets' } }
+
+ it 'returns only this author commits' do
+ get api(route, user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ author_names = json_response.map { |commit| commit['author_name'] }.uniq
+
+ expect(author_names).to contain_exactly('Dmitriy Zaporozhets')
+ end
+
+ context 'when author is missing' do
+ let(:params) { { author: '' } }
+
+ it 'returns all commits' do
+ get api(route, user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.count).to eq(20)
+ end
+ end
+
+ context 'when author does not exists' do
+ let(:params) { { author: 'does not exist' } }
+
+ it 'returns an empty list' do
+ get api(route, user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq([])
+ end
+ end
+ end
+
context 'when repository does not exist' do
let(:project) { create(:project, creator: user, path: 'my.project') }
@@ -425,6 +461,27 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
describe "POST /projects/:id/repository/commits" do
let!(:url) { "/projects/#{project_id}/repository/commits" }
+ context 'when unauthenticated', 'and project is public' do
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let(:params) do
+ {
+ branch: 'master',
+ commit_message: 'message',
+ actions: [
+ {
+ action: 'create',
+ file_path: '/test.rb',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+
+ it_behaves_like '401 response' do
+ let(:request) { post api(url), params: params }
+ end
+ end
+
it 'returns a 403 unauthorized for user without permissions' do
post api(url, guest)
@@ -523,7 +580,6 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
let(:property) { 'g_edit_by_web_ide' }
let(:label) { 'usage_activity_by_stage_monthly.create.action_monthly_active_users_ide_edit' }
let(:context) { [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event_name).to_context] }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
end
context 'counts.web_ide_commits Snowplow event tracking' do
@@ -536,7 +592,6 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
let(:category) { described_class.to_s }
let(:namespace) { project.namespace.reload }
let(:label) { 'counts.web_ide_commits' }
- let(:feature_flag_name) { 'route_hll_to_snowplow_phase3' }
let(:context) do
[Gitlab::Tracking::ServicePingContext.new(data_source: :redis, key_path: 'counts.web_ide_commits').to_context.to_json]
end
@@ -1776,7 +1831,7 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
context 'when unauthenticated', 'and project is public' do
let_it_be(:project) { create(:project, :public, :repository) }
- it_behaves_like '403 response' do
+ it_behaves_like '401 response' do
let(:request) { post api(route), params: { branch: 'master' } }
end
end
@@ -1956,7 +2011,7 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
context 'when unauthenticated', 'and project is public' do
let_it_be(:project) { create(:project, :public, :repository) }
- it_behaves_like '403 response' do
+ it_behaves_like '401 response' do
let(:request) { post api(route), params: { branch: branch } }
end
end
diff --git a/spec/requests/api/composer_packages_spec.rb b/spec/requests/api/composer_packages_spec.rb
index 0c726d46a01..2bb2ffa03c4 100644
--- a/spec/requests/api/composer_packages_spec.rb
+++ b/spec/requests/api/composer_packages_spec.rb
@@ -504,7 +504,11 @@ RSpec.describe API::ComposerPackages, feature_category: :package_registry do
include_context 'Composer user type', params[:user_role], params[:member] do
if params[:expected_status] == :success
let(:snowplow_gitlab_standard_context) do
- { project: project, namespace: project.namespace, property: 'i_package_composer_user' }
+ if user_role == :anonymous || (project_visibility_level == 'PUBLIC' && user_token == false)
+ { project: project, namespace: project.namespace, property: 'i_package_composer_user' }
+ else
+ { project: project, namespace: project.namespace, property: 'i_package_composer_user', user: user }
+ end
end
it_behaves_like 'a package tracking event', described_class.name, 'pull_package'
diff --git a/spec/requests/api/conan_project_packages_spec.rb b/spec/requests/api/conan_project_packages_spec.rb
index 814745f9e29..06f175233db 100644
--- a/spec/requests/api/conan_project_packages_spec.rb
+++ b/spec/requests/api/conan_project_packages_spec.rb
@@ -33,6 +33,29 @@ RSpec.describe API::ConanProjectPackages, feature_category: :package_registry do
subject { get api(url), params: params }
end
+
+ context 'with access to package registry for everyone' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
+
+ get api(url), params: params
+ end
+
+ subject { json_response['results'] }
+
+ context 'with a matching name' do
+ let(:params) { { q: package.conan_recipe } }
+
+ it { is_expected.to contain_exactly(package.conan_recipe) }
+ end
+
+ context 'with a * wildcard' do
+ let(:params) { { q: "#{package.name[0, 3]}*" } }
+
+ it { is_expected.to contain_exactly(package.conan_recipe) }
+ end
+ end
end
describe 'GET /api/v4/projects/:id/packages/conan/v1/users/authenticate' do
diff --git a/spec/requests/api/debian_group_packages_spec.rb b/spec/requests/api/debian_group_packages_spec.rb
index 0c80b7d830f..9c726e5a5f7 100644
--- a/spec/requests/api/debian_group_packages_spec.rb
+++ b/spec/requests/api/debian_group_packages_spec.rb
@@ -6,76 +6,119 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
include WorkhorseHelpers
include_context 'Debian repository shared context', :group, false do
+ shared_examples 'a Debian package tracking event' do |action|
+ include_context 'Debian repository access', :public, :developer, :basic do
+ let(:snowplow_gitlab_standard_context) do
+ { project: nil, namespace: container, user: user, property: 'i_package_debian_user' }
+ end
+
+ it_behaves_like 'a package tracking event', described_class.name, action
+ end
+ end
+
+ shared_examples 'not a Debian package tracking event' do
+ include_context 'Debian repository access', :public, :developer, :basic do
+ it_behaves_like 'not a package tracking event', described_class.name, /.*/
+ end
+ end
+
context 'with invalid parameter' do
let(:url) { "/groups/1/-/packages/debian/dists/with+space/InRelease" }
it_behaves_like 'Debian packages GET request', :bad_request, /^distribution is invalid$/
+ it_behaves_like 'not a Debian package tracking event'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release.gpg' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/Release.gpg" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNATURE-----/
+ it_behaves_like 'not a Debian package tracking event'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/Release" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Codename: fixture-distribution\n$/
+ it_behaves_like 'a Debian package tracking event', 'list_package'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/InRelease' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/InRelease" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNED MESSAGE-----/
+ it_behaves_like 'a Debian package tracking event', 'list_package'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
- let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages" }
+ let(:target_component_file) { component_file }
+ let(:target_component_name) { component.name }
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/binary-#{architecture.name}/Packages" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/
+ it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete Packages file/
+ it_behaves_like 'a Debian package tracking event', 'list_package'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages.gz' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages.gz" }
it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/
+ it_behaves_like 'not a Debian package tracking event'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do
- let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" }
+ let(:target_component_file) { component_file_older_sha256 }
+ let(:target_component_name) { component.name }
+ let(:target_sha256) { target_component_file.file_sha256 }
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/binary-#{architecture.name}/by-hash/SHA256/#{target_sha256}" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/
+ it_behaves_like 'a Debian package tracking event', 'list_package'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/source/Sources' do
- let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/source/Sources" }
+ let(:target_component_file) { component_file_sources }
+ let(:target_component_name) { component.name }
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/Sources" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Sources file/
+ it_behaves_like 'Debian packages index endpoint', /^Description: This is an incomplete Sources file$/
+ it_behaves_like 'a Debian package tracking event', 'list_package'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/source/by-hash/SHA256/:file_sha256' do
- let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/source/by-hash/SHA256/#{component_file_sources_older_sha256.file_sha256}" }
+ let(:target_component_file) { component_file_sources_older_sha256 }
+ let(:target_component_name) { component.name }
+ let(:target_sha256) { target_component_file.file_sha256 }
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/by-hash/SHA256/#{target_sha256}" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/
+ it_behaves_like 'a Debian package tracking event', 'list_package'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages' do
- let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages" }
+ let(:target_component_file) { component_file_di }
+ let(:target_component_name) { component.name }
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/debian-installer/binary-#{architecture.name}/Packages" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete D-I Packages file/
+ it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete D-I Packages file/
+ it_behaves_like 'a Debian package tracking event', 'list_package'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages.gz' do
let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages.gz" }
it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/
+ it_behaves_like 'not a Debian package tracking event'
end
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do
- let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" }
+ let(:target_component_file) { component_file_di_older_sha256 }
+ let(:target_component_name) { component.name }
+ let(:target_sha256) { target_component_file.file_sha256 }
+ let(:url) { "/groups/#{container.id}/-/packages/debian/dists/#{distribution.codename}/#{target_component_name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{target_sha256}" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/
+ it_behaves_like 'a Debian package tracking event', 'list_package'
end
describe 'GET groups/:id/-/packages/debian/pool/:codename/:project_id/:letter/:package_name/:package_version/:file_name' do
@@ -89,12 +132,14 @@ RSpec.describe API::DebianGroupPackages, feature_category: :package_registry do
'sample_1.2.3~alpha2.dsc' | /^Format: 3.0 \(native\)/
'libsample0_1.2.3~alpha2_amd64.deb' | /^!<arch>/
'sample-udeb_1.2.3~alpha2_amd64.udeb' | /^!<arch>/
+ 'sample-ddeb_1.2.3~alpha2_amd64.ddeb' | /^!<arch>/
'sample_1.2.3~alpha2_amd64.buildinfo' | /Build-Tainted-By/
'sample_1.2.3~alpha2_amd64.changes' | /urgency=medium/
end
with_them do
it_behaves_like 'Debian packages read endpoint', 'GET', :success, params[:success_body]
+ it_behaves_like 'a Debian package tracking event', 'pull_package'
context 'for bumping last downloaded at' do
include_context 'Debian repository access', :public, :developer, :basic do
diff --git a/spec/requests/api/debian_project_packages_spec.rb b/spec/requests/api/debian_project_packages_spec.rb
index 46f79efd928..030962044c6 100644
--- a/spec/requests/api/debian_project_packages_spec.rb
+++ b/spec/requests/api/debian_project_packages_spec.rb
@@ -6,6 +6,22 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
include WorkhorseHelpers
include_context 'Debian repository shared context', :project, false do
+ shared_examples 'a Debian package tracking event' do |action|
+ include_context 'Debian repository access', :public, :developer, :basic do
+ let(:snowplow_gitlab_standard_context) do
+ { project: container, namespace: container.namespace, user: user, property: 'i_package_debian_user' }
+ end
+
+ it_behaves_like 'a package tracking event', described_class.name, action
+ end
+ end
+
+ shared_examples 'not a Debian package tracking event' do
+ include_context 'Debian repository access', :public, :developer, :basic do
+ it_behaves_like 'not a package tracking event', described_class.name, /.*/
+ end
+ end
+
shared_examples 'accept GET request on private project with access to package registry for everyone' do
include_context 'Debian repository access', :private, :anonymous, :basic do
before do
@@ -20,12 +36,14 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:url) { "/projects/1/packages/debian/dists/with+space/InRelease" }
it_behaves_like 'Debian packages GET request', :bad_request, /^distribution is invalid$/
+ it_behaves_like 'not a Debian package tracking event'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/Release.gpg' do
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/Release.gpg" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNATURE-----/
+ it_behaves_like 'not a Debian package tracking event'
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
@@ -33,6 +51,7 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/Release" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Codename: fixture-distribution\n$/
+ it_behaves_like 'a Debian package tracking event', 'list_package'
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
@@ -40,13 +59,17 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/InRelease" }
it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^-----BEGIN PGP SIGNED MESSAGE-----/
+ it_behaves_like 'a Debian package tracking event', 'list_package'
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
- let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages" }
+ let(:target_component_file) { component_file }
+ let(:target_component_name) { component.name }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/binary-#{architecture.name}/Packages" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Packages file/
+ it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete Packages file/
+ it_behaves_like 'a Debian package tracking event', 'list_package'
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
@@ -54,33 +77,48 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/Packages.gz" }
it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/
+ it_behaves_like 'not a Debian package tracking event'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256' do
- let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/binary-#{architecture.name}/by-hash/SHA256/#{component_file_older_sha256.file_sha256}" }
+ let(:target_component_file) { component_file_older_sha256 }
+ let(:target_component_name) { component.name }
+ let(:target_sha256) { target_component_file.file_sha256 }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/binary-#{architecture.name}/by-hash/SHA256/#{target_sha256}" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/
+ it_behaves_like 'a Debian package tracking event', 'list_package'
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/source/Sources' do
- let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/source/Sources" }
+ let(:target_component_file) { component_file_sources }
+ let(:target_component_name) { component.name }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/Sources" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete Sources file/
+ it_behaves_like 'Debian packages index endpoint', /^Description: This is an incomplete Sources file$/
+ it_behaves_like 'a Debian package tracking event', 'list_package'
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/source/by-hash/SHA256/:file_sha256' do
- let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/source/by-hash/SHA256/#{component_file_sources_older_sha256.file_sha256}" }
+ let(:target_component_file) { component_file_sources_older_sha256 }
+ let(:target_component_name) { component.name }
+ let(:target_sha256) { target_component_file.file_sha256 }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/source/by-hash/SHA256/#{target_sha256}" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/
+ it_behaves_like 'a Debian package tracking event', 'list_package'
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages' do
- let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages" }
+ let(:target_component_file) { component_file_di }
+ let(:target_component_name) { component.name }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/debian-installer/binary-#{architecture.name}/Packages" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /Description: This is an incomplete D-I Packages file/
+ it_behaves_like 'Debian packages index endpoint', /Description: This is an incomplete D-I Packages file/
+ it_behaves_like 'a Debian package tracking event', 'list_package'
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
@@ -88,12 +126,17 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/Packages.gz" }
it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /Format gz is not supported/
+ it_behaves_like 'not a Debian package tracking event'
end
describe 'GET projects/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256' do
- let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{component.name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{component_file_di_older_sha256.file_sha256}" }
+ let(:target_component_file) { component_file_di_older_sha256 }
+ let(:target_component_name) { component.name }
+ let(:target_sha256) { target_component_file.file_sha256 }
+ let(:url) { "/projects/#{container.id}/packages/debian/dists/#{distribution.codename}/#{target_component_name}/debian-installer/binary-#{architecture.name}/by-hash/SHA256/#{target_sha256}" }
- it_behaves_like 'Debian packages read endpoint', 'GET', :success, /^Other SHA256$/
+ it_behaves_like 'Debian packages index sha256 endpoint', /^Other SHA256$/
+ it_behaves_like 'a Debian package tracking event', 'list_package'
it_behaves_like 'accept GET request on private project with access to package registry for everyone'
end
@@ -108,12 +151,14 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
'sample_1.2.3~alpha2.dsc' | /^Format: 3.0 \(native\)/
'libsample0_1.2.3~alpha2_amd64.deb' | /^!<arch>/
'sample-udeb_1.2.3~alpha2_amd64.udeb' | /^!<arch>/
+ 'sample-ddeb_1.2.3~alpha2_amd64.ddeb' | /^!<arch>/
'sample_1.2.3~alpha2_amd64.buildinfo' | /Build-Tainted-By/
'sample_1.2.3~alpha2_amd64.changes' | /urgency=medium/
end
with_them do
it_behaves_like 'Debian packages read endpoint', 'GET', :success, params[:success_body]
+ it_behaves_like 'a Debian package tracking event', 'pull_package'
context 'for bumping last downloaded at' do
include_context 'Debian repository access', :public, :developer, :basic do
@@ -130,17 +175,19 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
describe 'PUT projects/:id/packages/debian/:file_name' do
let(:method) { :put }
let(:url) { "/projects/#{container.id}/packages/debian/#{file_name}" }
- let(:snowplow_gitlab_standard_context) { { project: container, user: user, namespace: container.namespace } }
context 'with a deb' do
let(:file_name) { 'libsample0_1.2.3~alpha2_amd64.deb' }
it_behaves_like 'Debian packages write endpoint', 'upload', :created, nil
+ it_behaves_like 'Debian packages endpoint catching ObjectStorage::RemoteStoreError'
+ it_behaves_like 'a Debian package tracking event', 'push_package'
context 'with codename and component' do
let(:extra_params) { { distribution: distribution.codename, component: 'main' } }
it_behaves_like 'Debian packages write endpoint', 'upload', :created, nil
+ it_behaves_like 'a Debian package tracking event', 'push_package'
end
context 'with codename and without component' do
@@ -149,6 +196,8 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
include_context 'Debian repository access', :public, :developer, :basic do
it_behaves_like 'Debian packages GET request', :bad_request, /component is missing/
end
+
+ it_behaves_like 'not a Debian package tracking event'
end
end
@@ -157,13 +206,19 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
include_context 'Debian repository access', :public, :developer, :basic do
it_behaves_like "Debian packages upload request", :created, nil
+ end
- context 'with codename and component' do
- let(:extra_params) { { distribution: distribution.codename, component: 'main' } }
+ it_behaves_like 'a Debian package tracking event', 'push_package'
+ context 'with codename and component' do
+ let(:extra_params) { { distribution: distribution.codename, component: 'main' } }
+
+ include_context 'Debian repository access', :public, :developer, :basic do
it_behaves_like "Debian packages upload request", :bad_request,
- /^file_name Only debs and udebs can be directly added to a distribution$/
+ /^file_name Only debs, udebs and ddebs can be directly added to a distribution$/
end
+
+ it_behaves_like 'not a Debian package tracking event'
end
end
@@ -171,6 +226,7 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:file_name) { 'sample_1.2.3~alpha2_amd64.changes' }
it_behaves_like 'Debian packages write endpoint', 'upload', :created, nil
+ it_behaves_like 'a Debian package tracking event', 'push_package'
end
end
@@ -180,6 +236,7 @@ RSpec.describe API::DebianProjectPackages, feature_category: :package_registry d
let(:url) { "/projects/#{container.id}/packages/debian/#{file_name}/authorize" }
it_behaves_like 'Debian packages write endpoint', 'upload authorize', :created, nil
+ it_behaves_like 'not a Debian package tracking event'
end
end
end
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index 15880d920c5..18a9211df3e 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do
+RSpec.describe API::DeployKeys, :aggregate_failures, feature_category: :continuous_delivery do
let_it_be(:user) { create(:user) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:admin) { create(:admin) }
@@ -11,33 +11,29 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do
let_it_be(:project3) { create(:project, creator_id: user.id) }
let_it_be(:deploy_key) { create(:deploy_key, public: true) }
let_it_be(:deploy_key_private) { create(:deploy_key, public: false) }
+ let_it_be(:path) { '/deploy_keys' }
+ let_it_be(:project_path) { "/projects/#{project.id}#{path}" }
let!(:deploy_keys_project) do
create(:deploy_keys_project, project: project, deploy_key: deploy_key)
end
describe 'GET /deploy_keys' do
+ it_behaves_like 'GET request permissions for admin mode'
+
context 'when unauthenticated' do
it 'returns authentication error' do
- get api('/deploy_keys')
+ get api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
- context 'when authenticated as non-admin user' do
- it 'returns a 403 error' do
- get api('/deploy_keys', user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
-
context 'when authenticated as admin' do
- let_it_be(:pat) { create(:personal_access_token, user: admin) }
+ let_it_be(:pat) { create(:personal_access_token, :admin_mode, user: admin) }
def make_api_request(params = {})
- get api('/deploy_keys', personal_access_token: pat), params: params
+ get api(path, personal_access_token: pat), params: params
end
it 'returns all deploy keys' do
@@ -91,14 +87,18 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do
describe 'GET /projects/:id/deploy_keys' do
let(:deploy_key) { create(:deploy_key, public: true, user: admin) }
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:path) { project_path }
+ let(:failed_status_code) { :not_found }
+ end
+
def perform_request
- get api("/projects/#{project.id}/deploy_keys", admin)
+ get api(project_path, admin, admin_mode: true)
end
it 'returns array of ssh keys' do
perform_request
- expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(deploy_key.title)
@@ -117,31 +117,59 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do
end
describe 'GET /projects/:id/deploy_keys/:key_id' do
+ let_it_be(:path) { "#{project_path}/#{deploy_key.id}" }
+ let_it_be(:unfindable_path) { "#{project_path}/404" }
+
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:failed_status_code) { :not_found }
+ end
+
it 'returns a single key' do
- get api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
+ get api(path, admin, admin_mode: true)
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(deploy_key.title)
expect(json_response).not_to have_key(:projects_with_write_access)
end
it 'returns 404 Not Found with invalid ID' do
- get api("/projects/#{project.id}/deploy_keys/404", admin)
+ get api(unfindable_path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
+
+ context 'when deploy key has expiry date' do
+ let(:deploy_key) { create(:deploy_key, :expired, public: true) }
+ let(:deploy_keys_project) { create(:deploy_keys_project, project: project, deploy_key: deploy_key) }
+
+ it 'returns expiry date' do
+ get api("#{project_path}/#{deploy_key.id}", admin, admin_mode: true)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(Time.parse(json_response['expires_at'])).to be_like_time(deploy_key.expires_at)
+ end
+ end
end
describe 'POST /projects/:id/deploy_keys' do
+ around do |example|
+ freeze_time { example.run }
+ end
+
+ it_behaves_like 'POST request permissions for admin mode', :not_found do
+ let(:params) { attributes_for :another_key }
+ let(:path) { project_path }
+ let(:failed_status_code) { :not_found }
+ end
+
it 'does not create an invalid ssh key' do
- post api("/projects/#{project.id}/deploy_keys", admin), params: { title: 'invalid key' }
+ post api(project_path, admin, admin_mode: true), params: { title: 'invalid key' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('key is missing')
end
it 'does not create a key without title' do
- post api("/projects/#{project.id}/deploy_keys", admin), params: { key: 'some key' }
+ post api(project_path, admin, admin_mode: true), params: { key: 'some key' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('title is missing')
@@ -151,7 +179,7 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do
key_attrs = attributes_for :another_key
expect do
- post api("/projects/#{project.id}/deploy_keys", admin), params: key_attrs
+ post api(project_path, admin, admin_mode: true), params: key_attrs
end.to change { project.deploy_keys.count }.by(1)
new_key = project.deploy_keys.last
@@ -161,7 +189,7 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do
it 'returns an existing ssh key when attempting to add a duplicate' do
expect do
- post api("/projects/#{project.id}/deploy_keys", admin), params: { key: deploy_key.key, title: deploy_key.title }
+ post api(project_path, admin, admin_mode: true), params: { key: deploy_key.key, title: deploy_key.title }
end.not_to change { project.deploy_keys.count }
expect(response).to have_gitlab_http_status(:created)
@@ -169,7 +197,7 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do
it 'joins an existing ssh key to a new project' do
expect do
- post api("/projects/#{project2.id}/deploy_keys", admin), params: { key: deploy_key.key, title: deploy_key.title }
+ post api("/projects/#{project2.id}/deploy_keys", admin, admin_mode: true), params: { key: deploy_key.key, title: deploy_key.title }
end.to change { project2.deploy_keys.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -178,18 +206,34 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do
it 'accepts can_push parameter' do
key_attrs = attributes_for(:another_key).merge(can_push: true)
- post api("/projects/#{project.id}/deploy_keys", admin), params: key_attrs
+ post api(project_path, admin, admin_mode: true), params: key_attrs
expect(response).to have_gitlab_http_status(:created)
expect(json_response['can_push']).to eq(true)
end
+
+ it 'accepts expires_at parameter' do
+ key_attrs = attributes_for(:another_key).merge(expires_at: 2.days.since.iso8601)
+
+ post api(project_path, admin, admin_mode: true), params: key_attrs
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(Time.parse(json_response['expires_at'])).to be_like_time(2.days.since)
+ end
end
describe 'PUT /projects/:id/deploy_keys/:key_id' do
+ let(:path) { "#{project_path}/#{deploy_key.id}" }
let(:extra_params) { {} }
+ let(:admin_mode) { false }
+
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:params) { { title: 'new title', can_push: true } }
+ let(:failed_status_code) { :not_found }
+ end
subject do
- put api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", api_user), params: extra_params
+ put api(path, api_user, admin_mode: admin_mode), params: extra_params
end
context 'with non-admin' do
@@ -204,6 +248,7 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do
context 'with admin' do
let(:api_user) { admin }
+ let(:admin_mode) { true }
context 'public deploy key attached to project' do
let(:extra_params) { { title: 'new title', can_push: true } }
@@ -258,9 +303,13 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do
context 'public deploy key attached to project' do
let(:extra_params) { { title: 'new title', can_push: true } }
- it 'updates the title of the deploy key' do
- expect { subject }.to change { deploy_key.reload.title }.to 'new title'
- expect(response).to have_gitlab_http_status(:ok)
+ context 'with admin mode on' do
+ let(:admin_mode) { true }
+
+ it 'updates the title of the deploy key' do
+ expect { subject }.to change { deploy_key.reload.title }.to 'new title'
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
it 'updates can_push of deploy_keys_project' do
@@ -298,18 +347,22 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do
deploy_key
end
+ let(:path) { "#{project_path}/#{deploy_key.id}" }
+
+ it_behaves_like 'DELETE request permissions for admin mode' do
+ let(:failed_status_code) { :not_found }
+ end
+
it 'removes existing key from project' do
expect do
- delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
-
- expect(response).to have_gitlab_http_status(:no_content)
+ delete api(path, admin, admin_mode: true)
end.to change { project.deploy_keys.count }.by(-1)
end
context 'when the deploy key is public' do
it 'does not delete the deploy key' do
expect do
- delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
+ delete api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
end.not_to change { DeployKey.count }
@@ -322,7 +375,7 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do
context 'when the deploy key is only used by this project' do
it 'deletes the deploy key' do
expect do
- delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
+ delete api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
end.to change { DeployKey.count }.by(-1)
@@ -336,7 +389,7 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do
it 'does not delete the deploy key' do
expect do
- delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
+ delete api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
end.not_to change { DeployKey.count }
@@ -345,26 +398,31 @@ RSpec.describe API::DeployKeys, feature_category: :continuous_delivery do
end
it 'returns 404 Not Found with invalid ID' do
- delete api("/projects/#{project.id}/deploy_keys/404", admin)
+ delete api("#{project_path}/404", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
it_behaves_like '412 response' do
- let(:request) { api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) }
+ let(:request) { api("#{project_path}/#{deploy_key.id}", admin, admin_mode: true) }
end
end
describe 'POST /projects/:id/deploy_keys/:key_id/enable' do
- let(:project2) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:path) { "/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable" }
+ let_it_be(:params) { {} }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:failed_status_code) { :not_found }
+ end
context 'when the user can admin the project' do
it 'enables the key' do
expect do
- post api("/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable", admin)
+ post api(path, admin, admin_mode: true)
end.to change { project2.deploy_keys.count }.from(0).to(1)
- expect(response).to have_gitlab_http_status(:created)
expect(json_response['id']).to eq(deploy_key.id)
end
end
diff --git a/spec/requests/api/deploy_tokens_spec.rb b/spec/requests/api/deploy_tokens_spec.rb
index 4efe49e843f..c0e36bf03bf 100644
--- a/spec/requests/api/deploy_tokens_spec.rb
+++ b/spec/requests/api/deploy_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do
+RSpec.describe API::DeployTokens, :aggregate_failures, feature_category: :continuous_delivery do
let_it_be(:user) { create(:user) }
let_it_be(:creator) { create(:user) }
let_it_be(:project) { create(:project, creator_id: creator.id) }
@@ -17,26 +17,25 @@ RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do
describe 'GET /deploy_tokens' do
subject do
- get api('/deploy_tokens', user)
+ get api('/deploy_tokens', user, admin_mode: admin_mode)
response
end
- context 'when unauthenticated' do
- let(:user) { nil }
+ let_it_be(:admin_mode) { false }
- it { is_expected.to have_gitlab_http_status(:unauthorized) }
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:path) { '/deploy_tokens' }
end
- context 'when authenticated as non-admin user' do
- let(:user) { creator }
+ context 'when unauthenticated' do
+ let(:user) { nil }
- it { is_expected.to have_gitlab_http_status(:forbidden) }
+ it { is_expected.to have_gitlab_http_status(:unauthorized) }
end
context 'when authenticated as admin' do
let(:user) { create(:admin) }
-
- it { is_expected.to have_gitlab_http_status(:ok) }
+ let_it_be(:admin_mode) { true }
it 'returns all deploy tokens' do
subject
@@ -57,7 +56,7 @@ RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do
context 'and active=true' do
it 'only returns active deploy tokens' do
- get api('/deploy_tokens?active=true', user)
+ get api('/deploy_tokens?active=true', user, admin_mode: true)
token_ids = json_response.map { |token| token['id'] }
expect(response).to have_gitlab_http_status(:ok)
@@ -73,8 +72,10 @@ RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do
end
describe 'GET /projects/:id/deploy_tokens' do
+ let(:path) { "/projects/#{project.id}/deploy_tokens" }
+
subject do
- get api("/projects/#{project.id}/deploy_tokens", user)
+ get api(path, user)
response
end
@@ -134,8 +135,10 @@ RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do
end
describe 'GET /projects/:id/deploy_tokens/:token_id' do
+ let(:path) { "/projects/#{project.id}/deploy_tokens/#{deploy_token.id}" }
+
subject do
- get api("/projects/#{project.id}/deploy_tokens/#{deploy_token.id}", user)
+ get api(path, user)
response
end
@@ -183,8 +186,10 @@ RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do
end
describe 'GET /groups/:id/deploy_tokens' do
+ let(:path) { "/groups/#{group.id}/deploy_tokens" }
+
subject do
- get api("/groups/#{group.id}/deploy_tokens", user)
+ get api(path, user)
response
end
@@ -241,8 +246,10 @@ RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do
end
describe 'GET /groups/:id/deploy_tokens/:token_id' do
+ let(:path) { "/groups/#{group.id}/deploy_tokens/#{group_deploy_token.id}" }
+
subject do
- get api("/groups/#{group.id}/deploy_tokens/#{group_deploy_token.id}", user)
+ get api(path, user)
response
end
@@ -290,8 +297,10 @@ RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do
end
describe 'DELETE /projects/:id/deploy_tokens/:token_id' do
+ let(:path) { "/projects/#{project.id}/deploy_tokens/#{deploy_token.id}" }
+
subject do
- delete api("/projects/#{project.id}/deploy_tokens/#{deploy_token.id}", user)
+ delete api(path, user)
response
end
@@ -455,8 +464,10 @@ RSpec.describe API::DeployTokens, feature_category: :continuous_delivery do
end
describe 'DELETE /groups/:id/deploy_tokens/:token_id' do
+ let(:path) { "/groups/#{group.id}/deploy_tokens/#{group_deploy_token.id}" }
+
subject do
- delete api("/groups/#{group.id}/deploy_tokens/#{group_deploy_token.id}", user)
+ delete api(path, user)
response
end
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index efe76c9cfda..3ca54cd40d0 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -47,11 +47,15 @@ RSpec.describe API::Deployments, feature_category: :continuous_delivery do
end
context 'when forbidden order_by is specified' do
+ before do
+ stub_feature_flags(deployments_raise_updated_at_inefficient_error_override: false)
+ end
+
it 'returns an error' do
perform_request({ updated_before: 30.minutes.ago, updated_after: 90.minutes.ago, order_by: :id })
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to include('`updated_at` filter and `updated_at` sorting must be paired')
+ expect(json_response['message']).to include('`updated_at` filter requires `updated_at` sort')
end
end
end
diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb
index 5116f074894..888220c2251 100644
--- a/spec/requests/api/doorkeeper_access_spec.rb
+++ b/spec/requests/api/doorkeeper_access_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'doorkeeper access', feature_category: :authentication_and_authorization do
+RSpec.describe 'doorkeeper access', feature_category: :system_access do
let!(:user) { create(:user) }
let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) }
let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" }
diff --git a/spec/requests/api/draft_notes_spec.rb b/spec/requests/api/draft_notes_spec.rb
index e8f519e004d..3911bb8bc00 100644
--- a/spec/requests/api/draft_notes_spec.rb
+++ b/spec/requests/api/draft_notes_spec.rb
@@ -8,11 +8,16 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
let_it_be(:project) { create(:project, :public) }
let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
+ let_it_be(:private_project) { create(:project, :private) }
+ let_it_be(:private_merge_request) do
+ create(:merge_request, source_project: private_project, target_project: private_project)
+ end
+
let_it_be(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) }
let!(:draft_note_by_current_user) { create(:draft_note, merge_request: merge_request, author: user) }
let!(:draft_note_by_random_user) { create(:draft_note, merge_request: merge_request) }
- let_it_be(:api_stub) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}" }
+ let_it_be(:base_url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes" }
before do
project.add_developer(user)
@@ -20,13 +25,13 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
describe "Get a list of merge request draft notes" do
it "returns 200 OK status" do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes", user)
+ get api(base_url, user)
expect(response).to have_gitlab_http_status(:ok)
end
it "returns only draft notes authored by the current user" do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes", user)
+ get api(base_url, user)
draft_note_ids = json_response.pluck("id")
@@ -40,7 +45,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
context "when requesting an existing draft note by the user" do
before do
get api(
- "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{draft_note_by_current_user.id}",
+ "#{base_url}/#{draft_note_by_current_user.id}",
user
)
end
@@ -56,7 +61,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
context "when requesting a non-existent draft note" do
it "returns a 404 Not Found response" do
get api(
- "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{DraftNote.last.id + 1}",
+ "#{base_url}/#{DraftNote.last.id + 1}",
user
)
@@ -67,7 +72,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
context "when requesting an existing draft note by another user" do
it "returns a 404 Not Found response" do
get api(
- "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{draft_note_by_random_user.id}",
+ "#{base_url}/#{draft_note_by_random_user.id}",
user
)
@@ -83,7 +88,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
before do
delete api(
- "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{draft_note_by_current_user.id}",
+ "#{base_url}/#{draft_note_by_current_user.id}",
user
)
end
@@ -100,7 +105,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
context "when deleting a non-existent draft note" do
it "returns a 404 Not Found" do
delete api(
- "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{non_existing_record_id}",
+ "#{base_url}/#{non_existing_record_id}",
user
)
@@ -111,7 +116,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
context "when deleting a draft note by a different user" do
it "returns a 404 Not Found" do
delete api(
- "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{draft_note_by_random_user.id}",
+ "#{base_url}/#{draft_note_by_random_user.id}",
user
)
@@ -120,10 +125,152 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
end
end
+ def create_draft_note(params = {}, url = base_url)
+ post api(url, user), params: params
+ end
+
+ describe "Create a new draft note" do
+ let(:basic_create_params) do
+ {
+ note: "Example body string"
+ }
+ end
+
+ context "when creating a new draft note" do
+ context "with required params" do
+ it "returns 201 Created status" do
+ create_draft_note(basic_create_params)
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+
+ it "creates a new draft note with the submitted params" do
+ expect { create_draft_note(basic_create_params) }.to change { DraftNote.count }.by(1)
+
+ expect(json_response["note"]).to eq(basic_create_params[:note])
+ expect(json_response["merge_request_id"]).to eq(merge_request.id)
+ expect(json_response["author_id"]).to eq(user.id)
+ end
+ end
+
+ context "without required params" do
+ it "returns 400 Bad Request status" do
+ create_draft_note({})
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context "when providing a non-existing commit_id" do
+ it "returns a 400 Bad Request" do
+ create_draft_note(
+ basic_create_params.merge(
+ commit_id: 'bad SHA'
+ )
+ )
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context "when targeting a merge request the user doesn't have access to" do
+ it "returns a 404 Not Found" do
+ create_draft_note(
+ basic_create_params,
+ "/projects/#{private_project.id}/merge_requests/#{private_merge_request.iid}"
+ )
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context "when attempting to resolve a disscussion" do
+ context "when providing a non-existant ID" do
+ it "returns a 400 Bad Request" do
+ create_draft_note(
+ basic_create_params.merge(
+ resolve_discussion: true,
+ in_reply_to_discussion_id: non_existing_record_id
+ )
+ )
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context "when not providing an ID" do
+ it "returns a 400 Bad Request" do
+ create_draft_note(basic_create_params.merge(resolve_discussion: true))
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it "returns a validation error message" do
+ create_draft_note(basic_create_params.merge(resolve_discussion: true))
+
+ expect(response.body)
+ .to eq("{\"message\":{\"base\":[\"User is not allowed to resolve thread\"]}}")
+ end
+ end
+ end
+ end
+ end
+
+ def update_draft_note(params = {}, url = base_url)
+ put api("#{url}/#{draft_note_by_current_user.id}", user), params: params
+ end
+
+ describe "Update a draft note" do
+ let(:basic_update_params) do
+ {
+ note: "Example updated body string"
+ }
+ end
+
+ context "when updating an existing draft note" do
+ context "with required params" do
+ it "returns 200 Success status" do
+ update_draft_note(basic_update_params)
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+
+ it "updates draft note with the new content" do
+ update_draft_note(basic_update_params)
+
+ expect(json_response["note"]).to eq(basic_update_params[:note])
+ end
+ end
+
+ context "without including an update to the note body" do
+ it "returns the draft note with no changes" do
+ expect { update_draft_note({}) }
+ .not_to change { draft_note_by_current_user.note }
+ end
+ end
+
+ context "when updating a non-existent draft note" do
+ it "returns a 404 Not Found" do
+ put api("#{base_url}/#{non_existing_record_id}", user), params: basic_update_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context "when updating a draft note by a different user" do
+ it "returns a 404 Not Found" do
+ put api("#{base_url}/#{draft_note_by_random_user.id}", user), params: basic_update_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+
describe "Publishing a draft note" do
let(:publish_draft_note) do
put api(
- "#{api_stub}/draft_notes/#{draft_note_by_current_user.id}/publish",
+ "#{base_url}/#{draft_note_by_current_user.id}/publish",
user
)
end
@@ -144,7 +291,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
context "when publishing a non-existent draft note" do
it "returns a 404 Not Found" do
put api(
- "#{api_stub}/draft_notes/#{non_existing_record_id}/publish",
+ "#{base_url}/#{non_existing_record_id}/publish",
user
)
@@ -155,7 +302,7 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
context "when publishing a draft note by a different user" do
it "returns a 404 Not Found" do
put api(
- "#{api_stub}/draft_notes/#{draft_note_by_random_user.id}/publish",
+ "#{base_url}/#{draft_note_by_random_user.id}/publish",
user
)
@@ -175,4 +322,47 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
end
end
end
+
+ describe "Bulk publishing draft notes" do
+ let(:bulk_publish_draft_notes) do
+ post api(
+ "#{base_url}/bulk_publish",
+ user
+ )
+ end
+
+ let!(:draft_note_by_current_user_2) { create(:draft_note, merge_request: merge_request, author: user) }
+
+ context "when publishing an existing draft note by the user" do
+ it "returns 204 No Content status" do
+ bulk_publish_draft_notes
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+
+ it "publishes the specified draft notes" do
+ expect { bulk_publish_draft_notes }.to change { Note.count }.by(2)
+ expect(DraftNote.exists?(draft_note_by_current_user.id)).to eq(false)
+ expect(DraftNote.exists?(draft_note_by_current_user_2.id)).to eq(false)
+ end
+
+ it "only publishes the user's draft notes" do
+ bulk_publish_draft_notes
+
+ expect(DraftNote.exists?(draft_note_by_random_user.id)).to eq(true)
+ end
+ end
+
+ context "when DraftNotes::PublishService returns a non-success" do
+ it "returns an :internal_server_error and a message" do
+ expect_next_instance_of(DraftNotes::PublishService) do |instance|
+ expect(instance).to receive(:execute).and_return({ status: :failure, message: "Error message" })
+ end
+
+ bulk_publish_draft_notes
+
+ expect(response).to have_gitlab_http_status(:internal_server_error)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 6164555ad19..9a435b3bce9 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -72,30 +72,11 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do
end
context "when params[:search] is less than #{described_class::MIN_SEARCH_LENGTH} characters" do
- before do
- stub_feature_flags(environment_search_api_min_chars: false)
- end
-
- it 'returns a normal response' do
+ it 'returns with status 400' do
get api("/projects/#{project.id}/environments?search=ab", user)
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(0)
- end
-
- context 'and environment_search_api_min_chars flag is enabled for the project' do
- before do
- stub_feature_flags(environment_search_api_min_chars: project)
- end
-
- it 'returns with status 400' do
- get api("/projects/#{project.id}/environments?search=ab", user)
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to include("Search query is less than #{described_class::MIN_SEARCH_LENGTH} characters")
- end
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to include("Search query is less than #{described_class::MIN_SEARCH_LENGTH} characters")
end
end
@@ -229,14 +210,13 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do
end
describe 'PUT /projects/:id/environments/:environment_id' do
- it 'returns a 200 if name and external_url are changed' do
+ it 'returns a 200 if external_url is changed' do
url = 'https://mepmep.whatever.ninja'
put api("/projects/#{project.id}/environments/#{environment.id}", user),
- params: { name: 'Mepmep', external_url: url }
+ params: { external_url: url }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/environment')
- expect(json_response['name']).to eq('Mepmep')
expect(json_response['external_url']).to eq(url)
end
@@ -258,16 +238,6 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do
expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
end
- it "won't update the external_url if only the name is passed" do
- url = environment.external_url
- put api("/projects/#{project.id}/environments/#{environment.id}", user),
- params: { name: 'Mepmep' }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['name']).to eq('Mepmep')
- expect(json_response['external_url']).to eq(url)
- end
-
it 'returns a 404 if the environment does not exist' do
put api("/projects/#{project.id}/environments/#{non_existing_record_id}", user)
diff --git a/spec/requests/api/error_tracking/project_settings_spec.rb b/spec/requests/api/error_tracking/project_settings_spec.rb
index 5906cdf105a..bde90627983 100644
--- a/spec/requests/api/error_tracking/project_settings_spec.rb
+++ b/spec/requests/api/error_tracking/project_settings_spec.rb
@@ -4,9 +4,9 @@ require 'spec_helper'
RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tracking do
let_it_be(:user) { create(:user) }
-
- let(:setting) { create(:project_error_tracking_setting) }
- let(:project) { setting.project }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:setting) { create(:project_error_tracking_setting, project: project) }
+ let_it_be(:project_without_setting) { create(:project) }
shared_examples 'returns project settings' do
it 'returns correct project settings' do
@@ -38,7 +38,7 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
end
end
- shared_examples 'returns 404' do
+ shared_examples 'returns no project settings' do
it 'returns no project settings' do
make_request
@@ -48,8 +48,60 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
end
end
+ shared_examples 'returns 400' do
+ it 'rejects request' do
+ make_request
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ shared_examples 'returns 401' do
+ it 'rejects request' do
+ make_request
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ shared_examples 'returns 403' do
+ it 'rejects request' do
+ make_request
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ shared_examples 'returns 404' do
+ it 'rejects request' do
+ make_request
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ shared_examples 'returns 400 with `integrated` param required or invalid' do |error|
+ it 'returns 400' do
+ make_request
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error'])
+ .to eq(error)
+ end
+ end
+
+ shared_examples "returns error from UpdateService" do
+ it "returns errors" do
+ make_request
+
+ expect(json_response['http_status']).to eq('forbidden')
+ expect(json_response['message']).to eq('An error occurred')
+ end
+ end
+
describe "PATCH /projects/:id/error_tracking/settings" do
- let(:params) { { active: false } }
+ let(:params) { { active: false, integrated: integrated } }
+ let(:integrated) { false }
def make_request
patch api("/projects/#{project.id}/error_tracking/settings", user), params: params
@@ -60,95 +112,97 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
project.add_maintainer(user)
end
- context 'patch settings' do
- context 'integrated_error_tracking feature enabled' do
- it_behaves_like 'returns project settings'
- end
-
- context 'integrated_error_tracking feature disabled' do
- before do
- stub_feature_flags(integrated_error_tracking: false)
- end
+ context 'with integrated_error_tracking feature enabled' do
+ it_behaves_like 'returns project settings'
+ end
- it_behaves_like 'returns project settings with false for integrated'
+ context 'with integrated_error_tracking feature disabled' do
+ before do
+ stub_feature_flags(integrated_error_tracking: false)
end
- it 'updates enabled flag' do
- expect(setting).to be_enabled
+ it_behaves_like 'returns project settings with false for integrated'
+ end
- make_request
+ it 'updates enabled flag' do
+ expect(setting).to be_enabled
- expect(json_response).to include('active' => false)
- expect(setting.reload).not_to be_enabled
- end
+ make_request
+
+ expect(json_response).to include('active' => false)
+ expect(setting.reload).not_to be_enabled
+ end
- context 'active is invalid' do
- let(:params) { { active: "randomstring" } }
+ context 'when active is invalid' do
+ let(:params) { { active: "randomstring" } }
- it 'returns active is invalid if non boolean' do
- make_request
+ it 'returns active is invalid if non boolean' do
+ make_request
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error'])
- .to eq('active is invalid')
- end
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error'])
+ .to eq('active is invalid')
end
+ end
- context 'active is empty' do
- let(:params) { { active: '' } }
+ context 'when active is empty' do
+ let(:params) { { active: '' } }
- it 'returns 400' do
- make_request
+ it 'returns 400' do
+ make_request
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error'])
- .to eq('active is empty')
- end
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error'])
+ .to eq('active is empty')
end
+ end
- context 'with integrated param' do
- let(:params) { { active: true, integrated: true } }
+ context 'with integrated param' do
+ let(:params) { { active: true, integrated: true } }
- context 'integrated_error_tracking feature enabled' do
- before do
- stub_feature_flags(integrated_error_tracking: true)
- end
+ context 'when integrated_error_tracking feature enabled' do
+ before do
+ stub_feature_flags(integrated_error_tracking: true)
+ end
- it 'updates the integrated flag' do
- expect(setting.integrated).to be_falsey
+ it 'updates the integrated flag' do
+ expect(setting.integrated).to be_falsey
- make_request
+ make_request
- expect(json_response).to include('integrated' => true)
- expect(setting.reload.integrated).to be_truthy
- end
+ expect(json_response).to include('integrated' => true)
+ expect(setting.reload.integrated).to be_truthy
end
end
end
context 'without a project setting' do
- let(:project) { create(:project) }
+ let(:project) { project_without_setting }
before do
project.add_maintainer(user)
end
- context 'patch settings' do
- it_behaves_like 'returns 404'
- end
+ it_behaves_like 'returns no project settings'
end
- end
- context 'when authenticated as reporter' do
- before do
- project.add_reporter(user)
- end
+ context "when ::Projects::Operations::UpdateService responds with an error" do
+ before do
+ allow_next_instance_of(::Projects::Operations::UpdateService) do |service|
+ allow(service)
+ .to receive(:execute)
+ .and_return({ status: :error, message: 'An error occurred', http_status: :forbidden })
+ end
+ end
- context 'patch request' do
- it 'returns 403' do
- make_request
+ context "when integrated" do
+ let(:integrated) { true }
- expect(response).to have_gitlab_http_status(:forbidden)
+ it_behaves_like 'returns error from UpdateService'
+ end
+
+ context "without integrated" do
+ it_behaves_like 'returns error from UpdateService'
end
end
end
@@ -158,35 +212,17 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
project.add_developer(user)
end
- context 'patch request' do
- it 'returns 403' do
- make_request
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
- end
+ it_behaves_like 'returns 403'
end
context 'when authenticated as non-member' do
- context 'patch request' do
- it 'returns 404' do
- make_request
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
+ it_behaves_like 'returns 404'
end
context 'when unauthenticated' do
let(:user) { nil }
- context 'patch request' do
- it 'returns 401 for update request' do
- make_request
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
+ it_behaves_like 'returns 401'
end
end
@@ -200,77 +236,152 @@ RSpec.describe API::ErrorTracking::ProjectSettings, feature_category: :error_tra
project.add_maintainer(user)
end
- context 'get settings' do
- context 'integrated_error_tracking feature enabled' do
- before do
- stub_feature_flags(integrated_error_tracking: true)
- end
+ it_behaves_like 'returns project settings'
- it_behaves_like 'returns project settings'
+ context 'when integrated_error_tracking feature disabled' do
+ before do
+ stub_feature_flags(integrated_error_tracking: false)
end
- context 'integrated_error_tracking feature disabled' do
- before do
- stub_feature_flags(integrated_error_tracking: false)
- end
-
- it_behaves_like 'returns project settings with false for integrated'
- end
+ it_behaves_like 'returns project settings with false for integrated'
end
end
context 'without a project setting' do
- let(:project) { create(:project) }
+ let(:project) { project_without_setting }
before do
project.add_maintainer(user)
end
- context 'get settings' do
- it_behaves_like 'returns 404'
- end
+ it_behaves_like 'returns no project settings'
end
- context 'when authenticated as reporter' do
+ context 'when authenticated as developer' do
before do
- project.add_reporter(user)
+ project.add_developer(user)
end
- it 'returns 403' do
- make_request
+ it_behaves_like 'returns 403'
+ end
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ context 'when authenticated as non-member' do
+ it_behaves_like 'returns 404'
end
- context 'when authenticated as developer' do
- before do
- project.add_developer(user)
- end
+ context 'when unauthenticated' do
+ let(:user) { nil }
- it 'returns 403' do
- make_request
+ it_behaves_like 'returns 401'
+ end
+ end
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ describe "PUT /projects/:id/error_tracking/settings" do
+ let(:params) { { active: active, integrated: integrated } }
+ let(:active) { true }
+ let(:integrated) { true }
+
+ def make_request
+ put api("/projects/#{project.id}/error_tracking/settings", user), params: params
end
- context 'when authenticated as non-member' do
- it 'returns 404' do
- make_request
+ context 'when authenticated' do
+ context 'as maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context "when integrated" do
+ context "with existing setting" do
+ let(:project) { setting.project }
+ let(:setting) { create(:project_error_tracking_setting, :integrated) }
+ let(:active) { false }
+
+ it "updates a setting" do
+ expect { make_request }.not_to change { ErrorTracking::ProjectErrorTrackingSetting.count }
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(json_response).to eq(
+ "active" => false,
+ "api_url" => nil,
+ "integrated" => integrated,
+ "project_name" => nil,
+ "sentry_external_url" => nil
+ )
+ end
+ end
+
+ context "without setting" do
+ let(:project) { project_without_setting }
+ let(:active) { true }
+
+ it "creates a setting" do
+ expect { make_request }.to change { ErrorTracking::ProjectErrorTrackingSetting.count }
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(json_response).to eq(
+ "active" => true,
+ "api_url" => nil,
+ "integrated" => true,
+ "project_name" => nil,
+ "sentry_external_url" => nil
+ )
+ end
+ end
+
+ context "when ::Projects::Operations::UpdateService responds with an error" do
+ before do
+ allow_next_instance_of(::Projects::Operations::UpdateService) do |service|
+ allow(service)
+ .to receive(:execute)
+ .and_return({ status: :error, message: 'An error occurred', http_status: :forbidden })
+ end
+ end
+
+ it_behaves_like 'returns error from UpdateService'
+ end
+ end
+
+ context "when integrated_error_tracking feature disabled" do
+ before do
+ stub_feature_flags(integrated_error_tracking: false)
+ end
+
+ it_behaves_like 'returns 404'
+ end
+
+ context "when integrated param is invalid" do
+ let(:params) { { active: active, integrated: 'invalid_string' } }
+
+ it_behaves_like 'returns 400 with `integrated` param required or invalid', 'integrated is invalid'
+ end
+
+ context "when integrated param is missing" do
+ let(:params) { { active: active } }
+
+ it_behaves_like 'returns 400 with `integrated` param required or invalid', 'integrated is missing'
+ end
end
- end
- context 'when unauthenticated' do
- let(:user) { nil }
+ context "as developer" do
+ before do
+ project.add_developer(user)
+ end
- it 'returns 401' do
- make_request
+ it_behaves_like 'returns 403'
+ end
- expect(response).to have_gitlab_http_status(:unauthorized)
+ context 'as non-member' do
+ it_behaves_like 'returns 404'
end
end
+
+ context "when unauthorized" do
+ let(:user) { nil }
+
+ it_behaves_like 'returns 401'
+ end
end
end
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index f4066c54c47..ed84e3e5f48 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -55,6 +55,11 @@ RSpec.describe API::Files, feature_category: :source_code_management do
}
end
+ let(:last_commit_for_path) do
+ Gitlab::Git::Commit
+ .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
+ end
+
shared_context 'with author parameters' do
let(:author_email) { 'user@example.org' }
let(:author_name) { 'John Doe' }
@@ -136,6 +141,12 @@ RSpec.describe API::Files, feature_category: :source_code_management do
it 'caches sha256 of the content', :use_clean_rails_redis_caching do
head api(route(file_path), current_user, **options), params: params
+ expect(Gitlab::Cache::Client).to receive(:build_with_metadata).with(
+ cache_identifier: 'API::Files#content_sha',
+ feature_category: :source_code_management,
+ backing_resource: :gitaly
+ ).and_call_original
+
expect(Rails.cache.fetch("blob_content_sha256:#{project.full_path}:#{response.headers['X-Gitlab-Blob-Id']}"))
.to eq(content_sha256)
@@ -829,7 +840,6 @@ RSpec.describe API::Files, feature_category: :source_code_management do
expect_to_send_git_blob(api(url, current_user), params)
expect(response.headers['Cache-Control']).to eq('max-age=0, private, must-revalidate, no-store, no-cache')
- expect(response.headers['Pragma']).to eq('no-cache')
expect(response.headers['Expires']).to eq('Fri, 01 Jan 1990 00:00:00 GMT')
end
@@ -1180,7 +1190,7 @@ RSpec.describe API::Files, feature_category: :source_code_management do
end
context 'when updating an existing file with stale last commit id' do
- let(:params_with_stale_id) { params.merge(last_commit_id: 'stale') }
+ let(:params_with_stale_id) { params.merge(last_commit_id: last_commit_for_path.parent_id) }
it 'returns a 400 bad request' do
put api(route(file_path), user), params: params_with_stale_id
@@ -1191,12 +1201,7 @@ RSpec.describe API::Files, feature_category: :source_code_management do
end
context 'with correct last commit id' do
- let(:last_commit) do
- Gitlab::Git::Commit
- .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
- end
-
- let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) }
+ let(:params_with_correct_id) { params.merge(last_commit_id: last_commit_for_path.id) }
it 'updates existing file in project repo' do
put api(route(file_path), user), params: params_with_correct_id
@@ -1206,12 +1211,7 @@ RSpec.describe API::Files, feature_category: :source_code_management do
end
context 'when file path is invalid' do
- let(:last_commit) do
- Gitlab::Git::Commit
- .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
- end
-
- let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) }
+ let(:params_with_correct_id) { params.merge(last_commit_id: last_commit_for_path.id) }
it 'returns a 400 bad request' do
put api(route(invalid_file_path), user), params: params_with_correct_id
@@ -1222,12 +1222,7 @@ RSpec.describe API::Files, feature_category: :source_code_management do
end
it_behaves_like 'when path is absolute' do
- let(:last_commit) do
- Gitlab::Git::Commit
- .last_for_path(project.repository, 'master', Addressable::URI.unencode_component(file_path))
- end
-
- let(:params_with_correct_id) { params.merge(last_commit_id: last_commit.id) }
+ let(:params_with_correct_id) { params.merge(last_commit_id: last_commit_for_path.id) }
subject { put api(route(absolute_path), user), params: params_with_correct_id }
end
diff --git a/spec/requests/api/freeze_periods_spec.rb b/spec/requests/api/freeze_periods_spec.rb
index 170871706dc..b582c2e0f4e 100644
--- a/spec/requests/api/freeze_periods_spec.rb
+++ b/spec/requests/api/freeze_periods_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
+RSpec.describe API::FreezePeriods, :aggregate_failures, feature_category: :continuous_delivery do
let_it_be(:project) { create(:project, :repository, :private) }
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
@@ -12,11 +12,18 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
let(:last_freeze_period) { project.freeze_periods.last }
describe 'GET /projects/:id/freeze_periods' do
+ let(:path) { "/projects/#{project.id}/freeze_periods" }
+
+ it_behaves_like 'GET request permissions for admin mode' do
+ let!(:freeze_period) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) }
+ let(:failed_status_code) { :not_found }
+ end
+
context 'when the user is the admin' do
let!(:freeze_period) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) }
it 'returns 200 HTTP status' do
- get api("/projects/#{project.id}/freeze_periods", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
end
@@ -32,20 +39,20 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
let!(:freeze_period_2) { create(:ci_freeze_period, project: project, created_at: 1.day.ago) }
it 'returns 200 HTTP status' do
- get api("/projects/#{project.id}/freeze_periods", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
end
it 'returns freeze_periods ordered by created_at ascending' do
- get api("/projects/#{project.id}/freeze_periods", user)
+ get api(path, user)
expect(json_response.count).to eq(2)
expect(freeze_period_ids).to eq([freeze_period_1.id, freeze_period_2.id])
end
it 'matches response schema' do
- get api("/projects/#{project.id}/freeze_periods", user)
+ get api(path, user)
expect(response).to match_response_schema('public_api/v4/freeze_periods')
end
@@ -53,13 +60,13 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
context 'when there are no freeze_periods' do
it 'returns 200 HTTP status' do
- get api("/projects/#{project.id}/freeze_periods", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
end
it 'returns an empty response' do
- get api("/projects/#{project.id}/freeze_periods", user)
+ get api(path, user)
expect(json_response).to be_empty
end
@@ -76,7 +83,7 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
end
it 'responds 403 Forbidden' do
- get api("/projects/#{project.id}/freeze_periods", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -84,7 +91,7 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
context 'when user is not a project member' do
it 'responds 404 Not Found' do
- get api("/projects/#{project.id}/freeze_periods", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -93,7 +100,7 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
let(:project) { create(:project, :public) }
it 'responds 403 Forbidden' do
- get api("/projects/#{project.id}/freeze_periods", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -102,6 +109,16 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
end
describe 'GET /projects/:id/freeze_periods/:freeze_period_id' do
+ let(:path) { "/projects/#{project.id}/freeze_periods/#{freeze_period.id}" }
+
+ it_behaves_like 'GET request permissions for admin mode' do
+ let!(:freeze_period) do
+ create(:ci_freeze_period, project: project)
+ end
+
+ let(:failed_status_code) { :not_found }
+ end
+
context 'when there is a freeze period' do
let!(:freeze_period) do
create(:ci_freeze_period, project: project)
@@ -111,7 +128,7 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
let!(:freeze_period) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) }
it 'responds 200 OK' do
- get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
end
@@ -123,13 +140,13 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
end
it 'responds 200 OK' do
- get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
end
it 'returns a freeze period' do
- get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
+ get api(path, user)
expect(json_response).to include(
'id' => freeze_period.id,
@@ -139,7 +156,7 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
end
it 'matches response schema' do
- get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
+ get api(path, user)
expect(response).to match_response_schema('public_api/v4/freeze_period')
end
@@ -151,7 +168,7 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
end
it 'responds 403 Forbidden' do
- get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -161,7 +178,7 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
context 'when freeze_period exists' do
it 'responds 403 Forbidden' do
- get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -188,7 +205,15 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
}
end
- subject { post api("/projects/#{project.id}/freeze_periods", api_user), params: params }
+ let(:path) { "/projects/#{project.id}/freeze_periods" }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:failed_status_code) { :not_found }
+ end
+
+ subject do
+ post api(path, api_user, admin_mode: api_user.admin?), params: params
+ end
context 'when the user is the admin' do
let(:api_user) { admin }
@@ -310,7 +335,10 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
let(:params) { { freeze_start: '0 22 * * 5', freeze_end: '5 4 * * sun' } }
let!(:freeze_period) { create :ci_freeze_period, project: project }
- subject { put api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", api_user), params: params }
+ subject do
+ put api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", api_user, admin_mode: api_user.admin?),
+ params: params
+ end
context 'when user is the admin' do
let(:api_user) { admin }
@@ -397,7 +425,9 @@ RSpec.describe API::FreezePeriods, feature_category: :continuous_delivery do
let!(:freeze_period) { create :ci_freeze_period, project: project }
let(:freeze_period_id) { freeze_period.id }
- subject { delete api("/projects/#{project.id}/freeze_periods/#{freeze_period_id}", api_user) }
+ subject do
+ delete api("/projects/#{project.id}/freeze_periods/#{freeze_period_id}", api_user, admin_mode: api_user.admin?)
+ end
context 'when user is the admin' do
let(:api_user) { admin }
diff --git a/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb b/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb
new file mode 100644
index 00000000000..080f375245d
--- /dev/null
+++ b/spec/requests/api/graphql/achievements/user_achievements_query_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'UserAchievements', feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:non_revoked_achievement1) { create(:user_achievement, achievement: achievement, user: user) }
+ let_it_be(:non_revoked_achievement2) { create(:user_achievement, :revoked, achievement: achievement, user: user) }
+ let_it_be(:fields) do
+ <<~HEREDOC
+ id
+ achievements {
+ nodes {
+ userAchievements {
+ nodes {
+ id
+ achievement {
+ id
+ }
+ user {
+ id
+ }
+ awardedByUser {
+ id
+ }
+ revokedByUser {
+ id
+ }
+ }
+ }
+ }
+ }
+ HEREDOC
+ end
+
+ let_it_be(:query) do
+ graphql_query_for('namespace', { full_path: group.full_path }, fields)
+ end
+
+ before_all do
+ group.add_guest(user)
+ end
+
+ before do
+ post_graphql(query, current_user: user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns all non_revoked user_achievements' do
+ expect(graphql_data_at(:namespace, :achievements, :nodes, :userAchievements, :nodes))
+ .to contain_exactly(
+ a_graphql_entity_for(non_revoked_achievement1)
+ )
+ end
+
+ it 'can lookahead to eliminate N+1 queries', :use_clean_rails_memory_store_caching do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: user)
+ end.count
+
+ user2 = create(:user)
+ create(:user_achievement, achievement: achievement, user: user2)
+
+ expect { post_graphql(query, current_user: user) }.not_to exceed_all_query_limit(control_count)
+ end
+
+ context 'when the achievements feature flag is disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ post_graphql(query, current_user: user)
+ end
+
+ specify { expect(graphql_data_at(:namespace, :achievements, :nodes, :userAchievements, :nodes)).to be_empty }
+ end
+end
diff --git a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
index 95cabfea2fc..0437a30eccd 100644
--- a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
+++ b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
@@ -50,7 +50,6 @@ RSpec.describe 'Getting Ci Cd Setting', feature_category: :continuous_integratio
expect(settings_data['jobTokenScopeEnabled']).to eql project.ci_cd_settings.job_token_scope_enabled?
expect(settings_data['inboundJobTokenScopeEnabled']).to eql(
project.ci_cd_settings.inbound_job_token_scope_enabled?)
- expect(settings_data['optInJwt']).to eql project.ci_cd_settings.opt_in_jwt?
end
end
end
diff --git a/spec/requests/api/graphql/ci/config_variables_spec.rb b/spec/requests/api/graphql/ci/config_variables_spec.rb
index f76bb8ff837..4bad5dec684 100644
--- a/spec/requests/api/graphql/ci/config_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/config_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).ciConfigVariables(sha)', feature_category: :pipeline_authoring do
+RSpec.describe 'Query.project(fullPath).ciConfigVariables(ref)', feature_category: :secrets_management do
include GraphqlHelpers
include ReactiveCachingHelpers
@@ -20,7 +20,7 @@ RSpec.describe 'Query.project(fullPath).ciConfigVariables(sha)', feature_categor
%(
query {
project(fullPath: "#{project.full_path}") {
- ciConfigVariables(sha: "#{ref}") {
+ ciConfigVariables(ref: "#{ref}") {
key
value
valueOptions
diff --git a/spec/requests/api/graphql/ci/group_variables_spec.rb b/spec/requests/api/graphql/ci/group_variables_spec.rb
index d78b30787c9..3b8eeefb707 100644
--- a/spec/requests/api/graphql/ci/group_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/group_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.group(fullPath).ciVariables', feature_category: :pipeline_authoring do
+RSpec.describe 'Query.group(fullPath).ciVariables', feature_category: :secrets_management do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
diff --git a/spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb b/spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb
new file mode 100644
index 00000000000..3b4014c178c
--- /dev/null
+++ b/spec/requests/api/graphql/ci/inherited_ci_variables_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project(fullPath).inheritedCiVariables', feature_category: :secrets_management do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let_it_be(:project) { create(:project, group: subgroup) }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ inheritedCiVariables {
+ nodes {
+ id
+ key
+ environmentScope
+ groupName
+ groupCiCdSettingsPath
+ masked
+ protected
+ raw
+ variableType
+ }
+ }
+ }
+ }
+ )
+ end
+
+ def create_variables
+ create(:ci_group_variable, group: group)
+ create(:ci_group_variable, group: subgroup)
+ end
+
+ context 'when user is not a project maintainer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'returns nothing' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'inheritedCiVariables')).to be_nil
+ end
+ end
+
+ context 'when user is a project maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it "returns the project's CI variables inherited from its parent group and ancestors" do
+ group_var = create(:ci_group_variable, group: group, key: 'GROUP_VAR_A',
+ environment_scope: 'production', masked: false, protected: true, raw: true)
+
+ subgroup_var = create(:ci_group_variable, group: subgroup, key: 'SUBGROUP_VAR_B',
+ masked: true, protected: false, raw: false, variable_type: 'file')
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'inheritedCiVariables', 'nodes')).to eq([
+ {
+ 'id' => group_var.to_global_id.to_s,
+ 'key' => 'GROUP_VAR_A',
+ 'environmentScope' => 'production',
+ 'groupName' => group.name,
+ 'groupCiCdSettingsPath' => group_var.group_ci_cd_settings_path,
+ 'masked' => false,
+ 'protected' => true,
+ 'raw' => true,
+ 'variableType' => 'ENV_VAR'
+ },
+ {
+ 'id' => subgroup_var.to_global_id.to_s,
+ 'key' => 'SUBGROUP_VAR_B',
+ 'environmentScope' => '*',
+ 'groupName' => subgroup.name,
+ 'groupCiCdSettingsPath' => subgroup_var.group_ci_cd_settings_path,
+ 'masked' => true,
+ 'protected' => false,
+ 'raw' => false,
+ 'variableType' => 'FILE'
+ }
+ ])
+ end
+
+ it 'avoids N+1 database queries' do
+ create_variables
+
+ baseline = ActiveRecord::QueryRecorder.new do
+ run_with_clean_state(query, context: { current_user: user })
+ end
+
+ create_variables
+
+ multi = ActiveRecord::QueryRecorder.new do
+ run_with_clean_state(query, context: { current_user: user })
+ end
+
+ expect(multi).not_to exceed_query_limit(baseline)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb
index 5b65ae88426..a612b4c91b6 100644
--- a/spec/requests/api/graphql/ci/instance_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.ciVariables', feature_category: :pipeline_authoring do
+RSpec.describe 'Query.ciVariables', feature_category: :secrets_management do
include GraphqlHelpers
let(:query) do
diff --git a/spec/requests/api/graphql/ci/job_spec.rb b/spec/requests/api/graphql/ci/job_spec.rb
index 8121c5e5c85..960697db239 100644
--- a/spec/requests/api/graphql/ci/job_spec.rb
+++ b/spec/requests/api/graphql/ci/job_spec.rb
@@ -52,7 +52,8 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)', feature_category: :c
'duration' => 25,
'kind' => 'BUILD',
'queuedDuration' => 2.0,
- 'status' => job_2.status.upcase
+ 'status' => job_2.status.upcase,
+ 'failureMessage' => job_2.present.failure_message
)
end
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index 674407c0a0e..0d5ac725edd 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -1,6 +1,130 @@
# frozen_string_literal: true
require 'spec_helper'
+RSpec.describe 'Query.jobs', feature_category: :continuous_integration do
+ include GraphqlHelpers
+
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:runner) { create(:ci_runner) }
+ let_it_be(:build) do
+ create(:ci_build, pipeline: pipeline, name: 'my test job', ref: 'HEAD', tag_list: %w[tag1 tag2], runner: runner)
+ end
+
+ let(:query) do
+ %(
+ query {
+ jobs {
+ nodes {
+ id
+ #{fields.join(' ')}
+ }
+ }
+ }
+ )
+ end
+
+ let(:jobs_graphql_data) { graphql_data_at(:jobs, :nodes) }
+
+ let(:fields) do
+ %w[commitPath refPath webPath browseArtifactsPath playPath tags runner{id}]
+ end
+
+ it 'returns the paths in each job of a pipeline' do
+ post_graphql(query, current_user: admin)
+
+ expect(jobs_graphql_data).to contain_exactly(
+ a_graphql_entity_for(
+ build,
+ commit_path: "/#{project.full_path}/-/commit/#{build.sha}",
+ ref_path: "/#{project.full_path}/-/commits/HEAD",
+ web_path: "/#{project.full_path}/-/jobs/#{build.id}",
+ browse_artifacts_path: "/#{project.full_path}/-/jobs/#{build.id}/artifacts/browse",
+ play_path: "/#{project.full_path}/-/jobs/#{build.id}/play",
+ tags: build.tag_list,
+ runner: a_graphql_entity_for(runner)
+ )
+ )
+ end
+
+ context 'when requesting individual fields' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:admin2) { create(:admin) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:pipeline2) { create(:ci_pipeline, project: project2) }
+
+ where(:field) { fields }
+
+ with_them do
+ let(:fields) do
+ [field]
+ end
+
+ it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
+ # warm-up cache and so on:
+ args = { current_user: admin }
+ args2 = { current_user: admin2 }
+ post_graphql(query, **args2)
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, **args)
+ end
+
+ create(:ci_build, pipeline: pipeline2, name: 'my test job2', ref: 'HEAD', tag_list: %w[tag3])
+ post_graphql(query, **args)
+
+ expect { post_graphql(query, **args) }.not_to exceed_all_query_limit(control)
+ end
+ end
+ end
+end
+
+RSpec.describe 'Query.jobs.runner', feature_category: :continuous_integration do
+ include GraphqlHelpers
+
+ let_it_be(:admin) { create(:admin) }
+
+ let(:jobs_runner_graphql_data) { graphql_data_at(:jobs, :nodes, :runner) }
+ let(:query) do
+ %(
+ query {
+ jobs {
+ nodes {
+ runner{
+ id
+ adminUrl
+ description
+ }
+ }
+ }
+ }
+ )
+ end
+
+ context 'when job has no runner' do
+ let_it_be(:build) { create(:ci_build) }
+
+ it 'returns nil' do
+ post_graphql(query, current_user: admin)
+
+ expect(jobs_runner_graphql_data).to eq([nil])
+ end
+ end
+
+ context 'when job has runner' do
+ let_it_be(:runner) { create(:ci_runner) }
+ let_it_be(:build_with_runner) { create(:ci_build, runner: runner) }
+
+ it 'returns runner attributes' do
+ post_graphql(query, current_user: admin)
+
+ expect(jobs_runner_graphql_data).to contain_exactly(a_graphql_entity_for(runner, :description, 'adminUrl' => "http://localhost/admin/runners/#{runner.id}"))
+ end
+ end
+end
+
RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integration do
include GraphqlHelpers
@@ -260,6 +384,68 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati
end
end
+ describe '.jobs.runnerManager' do
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:runner_manager) { create(:ci_runner_machine, created_at: Time.current, contacted_at: Time.current) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:build) do
+ create(:ci_build, pipeline: pipeline, name: 'my test job', runner_manager: runner_manager)
+ end
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipeline(iid: "#{pipeline.iid}") {
+ jobs {
+ nodes {
+ id
+ name
+ runnerManager {
+ #{all_graphql_fields_for('CiRunnerManager', excluded: [:runner], max_depth: 1)}
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ let(:jobs_graphql_data) { graphql_data_at(:project, :pipeline, :jobs, :nodes) }
+
+ it 'returns the runner manager in each job of a pipeline' do
+ post_graphql(query, current_user: admin)
+
+ expect(jobs_graphql_data).to contain_exactly(
+ a_graphql_entity_for(
+ build,
+ name: build.name,
+ runner_manager: a_graphql_entity_for(
+ runner_manager,
+ system_id: runner_manager.system_xid,
+ created_at: runner_manager.created_at.iso8601,
+ contacted_at: runner_manager.contacted_at.iso8601,
+ status: runner_manager.status.to_s.upcase
+ )
+ )
+ )
+ end
+
+ it 'does not generate N+1 queries', :request_store, :use_sql_query_cache do
+ admin2 = create(:admin)
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: admin)
+ end
+
+ runner_manager2 = create(:ci_runner_machine)
+ create(:ci_build, pipeline: pipeline, name: 'my test job2', runner_manager: runner_manager2)
+
+ expect { post_graphql(query, current_user: admin2) }.not_to exceed_all_query_limit(control)
+ end
+ end
+
describe '.jobs.count' do
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:successful_job) { create(:ci_build, :success, pipeline: pipeline) }
diff --git a/spec/requests/api/graphql/ci/manual_variables_spec.rb b/spec/requests/api/graphql/ci/manual_variables_spec.rb
index 921c69e535d..47dccc0deb6 100644
--- a/spec/requests/api/graphql/ci/manual_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/manual_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables', feature_category: :pipeline_authoring do
+RSpec.describe 'Query.project(fullPath).pipelines.jobs.manualVariables', feature_category: :secrets_management do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/ci/project_variables_spec.rb b/spec/requests/api/graphql/ci/project_variables_spec.rb
index 0ddcac89b34..62fc2623a0f 100644
--- a/spec/requests/api/graphql/ci/project_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/project_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Query.project(fullPath).ciVariables', feature_category: :pipeline_authoring do
+RSpec.describe 'Query.project(fullPath).ciVariables', feature_category: :secrets_management do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/ci/runner_spec.rb b/spec/requests/api/graphql/ci/runner_spec.rb
index 986e3ce9e52..52b548ce8b9 100644
--- a/spec/requests/api/graphql/ci/runner_spec.rb
+++ b/spec/requests/api/graphql/ci/runner_spec.rb
@@ -6,11 +6,13 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
include GraphqlHelpers
let_it_be(:user) { create(:user, :admin) }
- let_it_be(:group) { create(:group) }
+ let_it_be(:another_admin) { create(:user, :admin) }
+ let_it_be_with_reload(:group) { create(:group) }
let_it_be(:active_instance_runner) do
- create(:ci_runner, :instance,
+ create(:ci_runner, :instance, :with_runner_manager,
description: 'Runner 1',
+ creator: user,
contacted_at: 2.hours.ago,
active: true,
version: 'adfe156',
@@ -28,6 +30,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let_it_be(:inactive_instance_runner) do
create(:ci_runner, :instance,
description: 'Runner 2',
+ creator: another_admin,
contacted_at: 1.day.ago,
active: false,
version: 'adfe157',
@@ -55,7 +58,9 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end
let_it_be(:project1) { create(:project) }
- let_it_be(:active_project_runner) { create(:ci_runner, :project, projects: [project1]) }
+ let_it_be(:active_project_runner) do
+ create(:ci_runner, :project, :with_runner_manager, projects: [project1])
+ end
shared_examples 'runner details fetch' do
let(:query) do
@@ -77,6 +82,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
expect(runner_data).to match a_graphql_entity_for(
runner,
description: runner.description,
+ created_by: runner.creator ? a_graphql_entity_for(runner.creator) : nil,
created_at: runner.created_at&.iso8601,
contacted_at: runner.contacted_at&.iso8601,
version: runner.version,
@@ -85,7 +91,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
locked: false,
active: runner.active,
paused: !runner.active,
- status: runner.status('14.5').to_s.upcase,
+ status: runner.status.to_s.upcase,
job_execution_status: runner.builds.running.any? ? 'RUNNING' : 'IDLE',
maximum_timeout: runner.maximum_timeout,
access_level: runner.access_level.to_s.upcase,
@@ -107,15 +113,39 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
),
project_count: nil,
admin_url: "http://localhost/admin/runners/#{runner.id}",
+ edit_admin_url: "http://localhost/admin/runners/#{runner.id}/edit",
+ register_admin_url: runner.registration_available? ? "http://localhost/admin/runners/#{runner.id}/register" : nil,
user_permissions: {
'readRunner' => true,
'updateRunner' => true,
'deleteRunner' => true,
'assignRunner' => true
- }
+ },
+ managers: a_hash_including(
+ "count" => runner.runner_managers.count,
+ "nodes" => an_instance_of(Array),
+ "pageInfo" => anything
+ )
)
expect(runner_data['tagList']).to match_array runner.tag_list
end
+
+ it 'does not execute more queries per runner', :use_sql_query_cache, :aggregate_failures do
+ # warm-up license cache and so on:
+ personal_access_token = create(:personal_access_token, user: user)
+ args = { current_user: user, token: { personal_access_token: personal_access_token } }
+ post_graphql(query, **args)
+ expect(graphql_data_at(:runner)).not_to be_nil
+
+ personal_access_token = create(:personal_access_token, user: another_admin)
+ args = { current_user: another_admin, token: { personal_access_token: personal_access_token } }
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) { post_graphql(query, **args) }
+
+ create(:ci_runner, :instance, version: '14.0.0', tag_list: %w[tag5 tag6], creator: another_admin)
+ create(:ci_runner, :project, version: '14.0.1', projects: [project1], tag_list: %w[tag3 tag8], creator: another_admin)
+
+ expect { post_graphql(query, **args) }.not_to exceed_all_query_limit(control)
+ end
end
shared_examples 'retrieval with no admin url' do
@@ -135,7 +165,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
runner_data = graphql_data_at(:runner)
expect(runner_data).not_to be_nil
- expect(runner_data).to match a_graphql_entity_for(runner, admin_url: nil)
+ expect(runner_data).to match a_graphql_entity_for(runner, admin_url: nil, edit_admin_url: nil)
expect(runner_data['tagList']).to match_array runner.tag_list
end
end
@@ -307,6 +337,24 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
it_behaves_like 'runner details fetch'
end
+ describe 'for registration type' do
+ context 'when registered with registration token' do
+ let(:runner) do
+ create(:ci_runner, registration_type: :registration_token)
+ end
+
+ it_behaves_like 'runner details fetch'
+ end
+
+ context 'when registered with authenticated user' do
+ let(:runner) do
+ create(:ci_runner, registration_type: :authenticated_user)
+ end
+
+ it_behaves_like 'runner details fetch'
+ end
+ end
+
describe 'for group runner request' do
let(:query) do
%(
@@ -330,24 +378,110 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end
end
- describe 'for runner with status' do
- let_it_be(:stale_runner) { create(:ci_runner, description: 'Stale runner 1', created_at: 3.months.ago) }
- let_it_be(:never_contacted_instance_runner) { create(:ci_runner, description: 'Missing runner 1', created_at: 1.month.ago, contacted_at: nil) }
-
- let(:status_fragment) do
+ describe 'ephemeralRegisterUrl' do
+ let(:runner_args) { { registration_type: :authenticated_user, creator: creator } }
+ let(:query) do
%(
- status
- legacyStatusWithExplicitVersion: status(legacyMode: "14.5")
- newStatus: status(legacyMode: null)
+ query {
+ runner(id: "#{runner.to_global_id}") {
+ ephemeralRegisterUrl
+ }
+ }
)
end
+ shared_examples 'has register url' do
+ it 'retrieves register url' do
+ post_graphql(query, current_user: user)
+ expect(graphql_data_at(:runner, :ephemeral_register_url)).to eq(expected_url)
+ end
+ end
+
+ shared_examples 'has no register url' do
+ it 'retrieves no register url' do
+ post_graphql(query, current_user: user)
+ expect(graphql_data_at(:runner, :ephemeral_register_url)).to eq(nil)
+ end
+ end
+
+ context 'with an instance runner', :freeze_time do
+ let(:creator) { user }
+ let(:runner) { create(:ci_runner, **runner_args) }
+
+ context 'with valid ephemeral registration' do
+ it_behaves_like 'has register url' do
+ let(:expected_url) { "http://localhost/admin/runners/#{runner.id}/register" }
+ end
+ end
+
+ context 'when runner ephemeral registration has expired' do
+ let(:runner) do
+ create(:ci_runner, created_at: (Ci::Runner::REGISTRATION_AVAILABILITY_TIME + 1.second).ago, **runner_args)
+ end
+
+ it_behaves_like 'has no register url'
+ end
+
+ context 'when runner has already been registered' do
+ let(:runner) { create(:ci_runner, :with_runner_manager, **runner_args) }
+
+ it_behaves_like 'has no register url'
+ end
+ end
+
+ context 'with a group runner' do
+ let(:creator) { user }
+ let(:runner) { create(:ci_runner, :group, groups: [group], **runner_args) }
+
+ context 'with valid ephemeral registration' do
+ it_behaves_like 'has register url' do
+ let(:expected_url) { "http://localhost/groups/#{group.path}/-/runners/#{runner.id}/register" }
+ end
+ end
+
+ context 'when request not from creator' do
+ let(:creator) { another_admin }
+
+ before do
+ group.add_owner(another_admin)
+ end
+
+ it_behaves_like 'has no register url'
+ end
+ end
+
+ context 'with a project runner' do
+ let(:creator) { user }
+ let(:runner) { create(:ci_runner, :project, projects: [project1], **runner_args) }
+
+ context 'with valid ephemeral registration' do
+ it_behaves_like 'has register url' do
+ let(:expected_url) { "http://localhost/#{project1.full_path}/-/runners/#{runner.id}/register" }
+ end
+ end
+
+ context 'when request not from creator' do
+ let(:creator) { another_admin }
+
+ before do
+ project1.add_owner(another_admin)
+ end
+
+ it_behaves_like 'has no register url'
+ end
+ end
+ end
+
+ describe 'for runner with status' do
+ let_it_be(:stale_runner) { create(:ci_runner, description: 'Stale runner 1', created_at: 3.months.ago) }
+ let_it_be(:never_contacted_instance_runner) { create(:ci_runner, description: 'Missing runner 1', created_at: 1.month.ago, contacted_at: nil) }
+
let(:query) do
%(
query {
- staleRunner: runner(id: "#{stale_runner.to_global_id}") { #{status_fragment} }
- pausedRunner: runner(id: "#{inactive_instance_runner.to_global_id}") { #{status_fragment} }
- neverContactedInstanceRunner: runner(id: "#{never_contacted_instance_runner.to_global_id}") { #{status_fragment} }
+ staleRunner: runner(id: "#{stale_runner.to_global_id}") { status }
+ pausedRunner: runner(id: "#{inactive_instance_runner.to_global_id}") { status }
+ neverContactedInstanceRunner: runner(id: "#{never_contacted_instance_runner.to_global_id}") { status }
}
)
end
@@ -357,23 +491,17 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
stale_runner_data = graphql_data_at(:stale_runner)
expect(stale_runner_data).to match a_hash_including(
- 'status' => 'STALE',
- 'legacyStatusWithExplicitVersion' => 'STALE',
- 'newStatus' => 'STALE'
+ 'status' => 'STALE'
)
paused_runner_data = graphql_data_at(:paused_runner)
expect(paused_runner_data).to match a_hash_including(
- 'status' => 'PAUSED',
- 'legacyStatusWithExplicitVersion' => 'PAUSED',
- 'newStatus' => 'OFFLINE'
+ 'status' => 'OFFLINE'
)
never_contacted_instance_runner_data = graphql_data_at(:never_contacted_instance_runner)
expect(never_contacted_instance_runner_data).to match a_hash_including(
- 'status' => 'NEVER_CONTACTED',
- 'legacyStatusWithExplicitVersion' => 'NEVER_CONTACTED',
- 'newStatus' => 'NEVER_CONTACTED'
+ 'status' => 'NEVER_CONTACTED'
)
end
end
@@ -568,34 +696,34 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end
end
- context 'with request made by creator' do
+ context 'with request made by creator', :frozen_time do
let(:user) { creator }
context 'with runner created in UI' do
let(:registration_type) { :authenticated_user }
- context 'with runner created in last 3 hours' do
- let(:created_at) { (3.hours - 1.second).ago }
+ context 'with runner created in last hour' do
+ let(:created_at) { (Ci::Runner::REGISTRATION_AVAILABILITY_TIME - 1.second).ago }
- context 'with no runner machine registed yet' do
+ context 'with no runner manager registered yet' do
it_behaves_like 'an ephemeral_authentication_token'
end
- context 'with first runner machine already registed' do
- let!(:runner_machine) { create(:ci_runner_machine, runner: runner) }
+ context 'with first runner manager already registered' do
+ let!(:runner_manager) { create(:ci_runner_machine, runner: runner) }
it_behaves_like 'a protected ephemeral_authentication_token'
end
end
context 'with runner created almost too long ago' do
- let(:created_at) { (3.hours - 1.second).ago }
+ let(:created_at) { (Ci::Runner::REGISTRATION_AVAILABILITY_TIME - 1.second).ago }
it_behaves_like 'an ephemeral_authentication_token'
end
context 'with runner created too long ago' do
- let(:created_at) { 3.hours.ago }
+ let(:created_at) { Ci::Runner::REGISTRATION_AVAILABILITY_TIME.ago }
it_behaves_like 'a protected ephemeral_authentication_token'
end
@@ -604,8 +732,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
context 'with runner registered from command line' do
let(:registration_type) { :registration_token }
- context 'with runner created in last 3 hours' do
- let(:created_at) { (3.hours - 1.second).ago }
+ context 'with runner created in last 1 hour' do
+ let(:created_at) { (Ci::Runner::REGISTRATION_AVAILABILITY_TIME - 1.second).ago }
it_behaves_like 'a protected ephemeral_authentication_token'
end
@@ -628,6 +756,12 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
<<~SINGLE
runner(id: "#{runner.to_global_id}") {
#{all_graphql_fields_for('CiRunner', excluded: excluded_fields)}
+ createdBy {
+ id
+ username
+ webPath
+ webUrl
+ }
groups {
nodes {
id
@@ -658,7 +792,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let(:active_group_runner2) { create(:ci_runner, :group) }
# Exclude fields that are already hardcoded above
- let(:excluded_fields) { %w[jobs groups projects ownerProject] }
+ let(:excluded_fields) { %w[createdBy jobs groups projects ownerProject] }
let(:single_query) do
<<~QUERY
@@ -691,6 +825,8 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
control = ActiveRecord::QueryRecorder.new { post_graphql(single_query, **args) }
+ personal_access_token = create(:personal_access_token, user: another_admin)
+ args = { current_user: another_admin, token: { personal_access_token: personal_access_token } }
expect { post_graphql(double_query, **args) }.not_to exceed_query_limit(control)
expect(graphql_data.count).to eq 6
@@ -721,20 +857,20 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
end
describe 'Query limits with jobs' do
- let!(:group1) { create(:group) }
- let!(:group2) { create(:group) }
- let!(:project1) { create(:project, :repository, group: group1) }
- let!(:project2) { create(:project, :repository, group: group1) }
- let!(:project3) { create(:project, :repository, group: group2) }
+ let_it_be(:group1) { create(:group) }
+ let_it_be(:group2) { create(:group) }
+ let_it_be(:project1) { create(:project, :repository, group: group1) }
+ let_it_be(:project2) { create(:project, :repository, group: group1) }
+ let_it_be(:project3) { create(:project, :repository, group: group2) }
- let!(:merge_request1) { create(:merge_request, source_project: project1) }
- let!(:merge_request2) { create(:merge_request, source_project: project3) }
+ let_it_be(:merge_request1) { create(:merge_request, source_project: project1) }
+ let_it_be(:merge_request2) { create(:merge_request, source_project: project3) }
let(:project_runner2) { create(:ci_runner, :project, projects: [project1, project2]) }
let!(:build1) { create(:ci_build, :success, name: 'Build One', runner: project_runner2, pipeline: pipeline1) }
- let!(:pipeline1) do
+ let_it_be(:pipeline1) do
create(:ci_pipeline, project: project1, source: :merge_request_event, merge_request: merge_request1, ref: 'main',
- target_sha: 'xxx')
+ target_sha: 'xxx')
end
let(:query) do
@@ -745,24 +881,7 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
jobs {
nodes {
id
- detailedStatus {
- id
- detailsPath
- group
- icon
- text
- }
- project {
- id
- name
- webUrl
- }
- shortSha
- commitPath
- finishedAt
- duration
- queuedDuration
- tags
+ #{field}
}
}
}
@@ -770,42 +889,69 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
QUERY
end
- it 'does not execute more queries per job', :aggregate_failures do
- # warm-up license cache and so on:
- personal_access_token = create(:personal_access_token, user: user)
- args = { current_user: user, token: { personal_access_token: personal_access_token } }
- post_graphql(query, **args)
-
- control = ActiveRecord::QueryRecorder.new(query_recorder_debug: true) { post_graphql(query, **args) }
-
- # Add a new build to project_runner2
- project_runner2.runner_projects << build(:ci_runner_project, runner: project_runner2, project: project3)
- pipeline2 = create(:ci_pipeline, project: project3, source: :merge_request_event, merge_request: merge_request2,
- ref: 'main', target_sha: 'xxx')
- build2 = create(:ci_build, :success, name: 'Build Two', runner: project_runner2, pipeline: pipeline2)
+ context 'when requesting individual fields' do
+ using RSpec::Parameterized::TableSyntax
- args[:current_user] = create(:user, :admin) # do not reuse same user
- expect { post_graphql(query, **args) }.not_to exceed_all_query_limit(control)
+ where(:field) do
+ [
+ 'detailedStatus { id detailsPath group icon text }',
+ 'project { id name webUrl }'
+ ] + %w[
+ shortSha
+ browseArtifactsPath
+ commitPath
+ playPath
+ refPath
+ webPath
+ finishedAt
+ duration
+ queuedDuration
+ tags
+ ]
+ end
- expect(graphql_data.count).to eq 1
- expect(graphql_data).to match(
- a_hash_including(
- 'runner' => a_graphql_entity_for(
- project_runner2,
- jobs: { 'nodes' => containing_exactly(a_graphql_entity_for(build1), a_graphql_entity_for(build2)) }
- )
- ))
+ with_them do
+ it 'does not execute more queries per job', :use_sql_query_cache, :aggregate_failures do
+ admin2 = create(:user, :admin) # do not reuse same user
+
+ # warm-up license cache and so on:
+ personal_access_token = create(:personal_access_token, user: user)
+ personal_access_token2 = create(:personal_access_token, user: admin2)
+ args = { current_user: user, token: { personal_access_token: personal_access_token } }
+ args2 = { current_user: admin2, token: { personal_access_token: personal_access_token2 } }
+ post_graphql(query, **args2)
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) { post_graphql(query, **args) }
+
+ # Add a new build to project_runner2
+ project_runner2.runner_projects << build(:ci_runner_project, runner: project_runner2, project: project3)
+ pipeline2 = create(:ci_pipeline, project: project3, source: :merge_request_event, merge_request: merge_request2,
+ ref: 'main', target_sha: 'xxx')
+ build2 = create(:ci_build, :success, name: 'Build Two', runner: project_runner2, pipeline: pipeline2)
+
+ expect { post_graphql(query, **args2) }.not_to exceed_all_query_limit(control)
+
+ expect(graphql_data.count).to eq 1
+ expect(graphql_data).to match(
+ a_hash_including(
+ 'runner' => a_graphql_entity_for(
+ project_runner2,
+ jobs: { 'nodes' => containing_exactly(a_graphql_entity_for(build1), a_graphql_entity_for(build2)) }
+ )
+ ))
+ end
+ end
end
end
describe 'sorting and pagination' do
let(:query) do
<<~GQL
- query($id: CiRunnerID!, $projectSearchTerm: String, $n: Int, $cursor: String) {
- runner(id: $id) {
- #{fields}
+ query($id: CiRunnerID!, $projectSearchTerm: String, $n: Int, $cursor: String) {
+ runner(id: $id) {
+ #{fields}
+ }
}
- }
GQL
end
@@ -824,18 +970,18 @@ RSpec.describe 'Query.runner(id)', feature_category: :runner_fleet do
let(:fields) do
<<~QUERY
- projects(search: $projectSearchTerm, first: $n, after: $cursor) {
- count
- nodes {
- id
- }
- pageInfo {
- hasPreviousPage
- startCursor
- endCursor
- hasNextPage
+ projects(search: $projectSearchTerm, first: $n, after: $cursor) {
+ count
+ nodes {
+ id
+ }
+ pageInfo {
+ hasPreviousPage
+ startCursor
+ endCursor
+ hasNextPage
+ }
}
- }
QUERY
end
diff --git a/spec/requests/api/graphql/ci/runners_spec.rb b/spec/requests/api/graphql/ci/runners_spec.rb
index 75d8609dc38..c8706ae9698 100644
--- a/spec/requests/api/graphql/ci/runners_spec.rb
+++ b/spec/requests/api/graphql/ci/runners_spec.rb
@@ -11,16 +11,24 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do
let_it_be(:instance_runner) { create(:ci_runner, :instance, version: 'abc', revision: '123', description: 'Instance runner', ip_address: '127.0.0.1') }
let_it_be(:project_runner) { create(:ci_runner, :project, active: false, version: 'def', revision: '456', description: 'Project runner', projects: [project], ip_address: '127.0.0.1') }
- let(:runners_graphql_data) { graphql_data['runners'] }
+ let(:runners_graphql_data) { graphql_data_at(:runners) }
let(:params) { {} }
let(:fields) do
<<~QUERY
nodes {
- #{all_graphql_fields_for('CiRunner', excluded: %w[ownerProject])}
+ #{all_graphql_fields_for('CiRunner', excluded: %w[createdBy ownerProject])}
+ createdBy {
+ username
+ webPath
+ webUrl
+ }
ownerProject {
id
+ path
+ fullPath
+ webUrl
}
}
QUERY
@@ -50,6 +58,25 @@ RSpec.describe 'Query.runners', feature_category: :runner_fleet do
it 'returns expected runner' do
expect(runners_graphql_data['nodes']).to contain_exactly(a_graphql_entity_for(expected_runner))
end
+
+ it 'does not execute more queries per runner', :aggregate_failures do
+ # warm-up license cache and so on:
+ personal_access_token = create(:personal_access_token, user: current_user)
+ args = { current_user: current_user, token: { personal_access_token: personal_access_token } }
+ post_graphql(query, **args)
+ expect(graphql_data_at(:runners, :nodes)).not_to be_empty
+
+ admin2 = create(:admin)
+ personal_access_token = create(:personal_access_token, user: admin2)
+ args = { current_user: admin2, token: { personal_access_token: personal_access_token } }
+ control = ActiveRecord::QueryRecorder.new { post_graphql(query, **args) }
+
+ create(:ci_runner, :instance, version: '14.0.0', tag_list: %w[tag5 tag6], creator: admin2)
+ create(:ci_runner, :project, version: '14.0.1', projects: [project], tag_list: %w[tag3 tag8],
+ creator: current_user)
+
+ expect { post_graphql(query, **args) }.not_to exceed_query_limit(control)
+ end
end
context 'runner_type is INSTANCE_TYPE and status is ACTIVE' do
diff --git a/spec/requests/api/graphql/current_user/todos_query_spec.rb b/spec/requests/api/graphql/current_user/todos_query_spec.rb
index f7e23aeb241..ee019a99f8d 100644
--- a/spec/requests/api/graphql/current_user/todos_query_spec.rb
+++ b/spec/requests/api/graphql/current_user/todos_query_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'Query current user todos', feature_category: :source_code_manage
let(:fields) do
<<~QUERY
nodes {
- #{all_graphql_fields_for('todos'.classify, max_depth: 2)}
+ #{all_graphql_fields_for('todos'.classify, max_depth: 2, excluded: ['productAnalyticsState'])}
}
QUERY
end
diff --git a/spec/requests/api/graphql/current_user_query_spec.rb b/spec/requests/api/graphql/current_user_query_spec.rb
index 53d2580caee..aceef77920d 100644
--- a/spec/requests/api/graphql/current_user_query_spec.rb
+++ b/spec/requests/api/graphql/current_user_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting project information', feature_category: :authentication_and_authorization do
+RSpec.describe 'getting project information', feature_category: :system_access do
include GraphqlHelpers
let(:fields) do
diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb
index 7b804623e01..1858ea831dd 100644
--- a/spec/requests/api/graphql/custom_emoji_query_spec.rb
+++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting custom emoji within namespace', feature_category: :not_owned do
+RSpec.describe 'getting custom emoji within namespace', feature_category: :shared do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/group/data_transfer_spec.rb b/spec/requests/api/graphql/group/data_transfer_spec.rb
new file mode 100644
index 00000000000..b7c038afa54
--- /dev/null
+++ b/spec/requests/api/graphql/group/data_transfer_spec.rb
@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'group data transfers', feature_category: :source_code_management do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project_1) { create(:project, group: group) }
+ let_it_be(:project_2) { create(:project, group: group) }
+
+ let(:fields) do
+ <<~QUERY
+ #{all_graphql_fields_for('GroupDataTransfer'.classify)}
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'group',
+ { fullPath: group.full_path },
+ query_graphql_field('DataTransfer', params, fields)
+ )
+ end
+
+ let(:from) { Date.new(2022, 1, 1) }
+ let(:to) { Date.new(2023, 1, 1) }
+ let(:params) { { from: from, to: to } }
+ let(:egress_data) do
+ graphql_data.dig('group', 'dataTransfer', 'egressNodes', 'nodes')
+ end
+
+ before do
+ create(:project_data_transfer, project: project_1, date: '2022-01-01', repository_egress: 1)
+ create(:project_data_transfer, project: project_1, date: '2022-02-01', repository_egress: 2)
+ create(:project_data_transfer, project: project_2, date: '2022-02-01', repository_egress: 4)
+ end
+
+ subject { post_graphql(query, current_user: current_user) }
+
+ context 'with anonymous access' do
+ let_it_be(:current_user) { nil }
+
+ before do
+ subject
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns no data' do
+ expect(graphql_data_at(:group, :data_transfer)).to be_nil
+ expect(graphql_errors).to be_nil
+ end
+ end
+
+ context 'with authorized user but without enough permissions' do
+ before do
+ group.add_developer(current_user)
+ subject
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns empty results' do
+ expect(graphql_data_at(:group, :data_transfer)).to be_nil
+ expect(graphql_errors).to be_nil
+ end
+ end
+
+ context 'when user has enough permissions' do
+ before do
+ group.add_owner(current_user)
+ end
+
+ context 'when data_transfer_monitoring_mock_data is NOT enabled' do
+ before do
+ stub_feature_flags(data_transfer_monitoring_mock_data: false)
+ subject
+ end
+
+ it 'returns real results' do
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(egress_data.count).to eq(2)
+
+ expect(egress_data.first.keys).to match_array(
+ %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
+ )
+
+ expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 6])
+ end
+
+ it_behaves_like 'a working graphql query'
+ end
+
+ context 'when data_transfer_monitoring_mock_data is enabled' do
+ before do
+ stub_feature_flags(data_transfer_monitoring_mock_data: true)
+ subject
+ end
+
+ it 'returns mock results' do
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(egress_data.count).to eq(12)
+ expect(egress_data.first.keys).to match_array(
+ %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
+ )
+ end
+
+ it_behaves_like 'a working graphql query'
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb
index 2c4770a31a7..a6eb114a279 100644
--- a/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb
+++ b/spec/requests/api/graphql/group/dependency_proxy_blobs_spec.rb
@@ -26,6 +26,7 @@ RSpec.describe 'getting dependency proxy blobs in a group', feature_category: :d
#{query_graphql_field('dependency_proxy_blobs', {}, dependency_proxy_blob_fields)}
dependencyProxyBlobCount
dependencyProxyTotalSize
+ dependencyProxyTotalSizeInBytes
GQL
end
@@ -42,6 +43,7 @@ RSpec.describe 'getting dependency proxy blobs in a group', feature_category: :d
let(:dependency_proxy_blobs_response) { graphql_data.dig('group', 'dependencyProxyBlobs', 'edges') }
let(:dependency_proxy_blob_count_response) { graphql_data.dig('group', 'dependencyProxyBlobCount') }
let(:dependency_proxy_total_size_response) { graphql_data.dig('group', 'dependencyProxyTotalSize') }
+ let(:dependency_proxy_total_size_in_bytes_response) { graphql_data.dig('group', 'dependencyProxyTotalSizeInBytes') }
before do
stub_config(dependency_proxy: { enabled: true })
@@ -121,7 +123,13 @@ RSpec.describe 'getting dependency proxy blobs in a group', feature_category: :d
it 'returns the total size' do
subject
+ expected_size = ActiveSupport::NumberHelper.number_to_human_size(blobs.inject(0) { |sum, blob| sum + blob.size })
+ expect(dependency_proxy_total_size_response).to eq(expected_size)
+ end
+
+ it 'returns the total size in bytes' do
+ subject
expected_size = blobs.inject(0) { |sum, blob| sum + blob.size }
- expect(dependency_proxy_total_size_response).to eq(ActiveSupport::NumberHelper.number_to_human_size(expected_size))
+ expect(dependency_proxy_total_size_in_bytes_response).to eq(expected_size)
end
end
diff --git a/spec/requests/api/graphql/group/labels_query_spec.rb b/spec/requests/api/graphql/group/labels_query_spec.rb
deleted file mode 100644
index 28886f8d80b..00000000000
--- a/spec/requests/api/graphql/group/labels_query_spec.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'getting group label information', feature_category: :team_planning do
- include GraphqlHelpers
-
- let_it_be(:group) { create(:group, :public) }
- let_it_be(:label_factory) { :group_label }
- let_it_be(:label_attrs) { { group: group } }
-
- it_behaves_like 'querying a GraphQL type with labels' do
- let(:path_prefix) { ['group'] }
-
- def make_query(fields)
- graphql_query_for('group', { full_path: group.full_path }, fields)
- end
- end
-end
diff --git a/spec/requests/api/graphql/group/milestones_spec.rb b/spec/requests/api/graphql/group/milestones_spec.rb
index 28cd68493c0..209588835f2 100644
--- a/spec/requests/api/graphql/group/milestones_spec.rb
+++ b/spec/requests/api/graphql/group/milestones_spec.rb
@@ -35,12 +35,6 @@ RSpec.describe 'Milestones through GroupQuery', feature_category: :team_planning
end
context 'when filtering by timeframe' do
- it 'fetches milestones between start_date and due_date' do
- fetch_milestones(user, { start_date: now.to_s, end_date: (now + 2.days).to_s })
-
- expect_array_response(milestone_2.to_global_id.to_s, milestone_3.to_global_id.to_s)
- end
-
it 'fetches milestones between timeframe start and end arguments' do
today = Date.today
fetch_milestones(user, { timeframe: { start: today.to_s, end: (today + 2.days).to_s } })
diff --git a/spec/requests/api/graphql/issues_spec.rb b/spec/requests/api/graphql/issues_spec.rb
index e437e1bbcb0..a12049a9b2e 100644
--- a/spec/requests/api/graphql/issues_spec.rb
+++ b/spec/requests/api/graphql/issues_spec.rb
@@ -15,6 +15,7 @@ RSpec.describe 'getting an issue list at root level', feature_category: :team_pl
let_it_be(:project_b) { create(:project, :repository, :private, group: group1) }
let_it_be(:project_c) { create(:project, :repository, :public, group: group2) }
let_it_be(:project_d) { create(:project, :repository, :private, group: group2) }
+ let_it_be(:archived_project) { create(:project, :repository, :archived, group: group2) }
let_it_be(:milestone1) { create(:milestone, project: project_c, due_date: 10.days.from_now) }
let_it_be(:milestone2) { create(:milestone, project: project_d, due_date: 20.days.from_now) }
let_it_be(:milestone3) { create(:milestone, project: project_d, due_date: 30.days.from_now) }
@@ -83,6 +84,7 @@ RSpec.describe 'getting an issue list at root level', feature_category: :team_pl
)
end
+ let_it_be(:archived_issue) { create(:issue, project: archived_project) }
let_it_be(:issues, reload: true) { [issue_a, issue_b, issue_c, issue_d, issue_e] }
# we need to always provide at least one filter to the query so it doesn't fail
let_it_be(:base_params) { { iids: issues.map { |issue| issue.iid.to_s } } }
@@ -109,6 +111,38 @@ RSpec.describe 'getting an issue list at root level', feature_category: :team_pl
end
end
+ describe 'includeArchived filter' do
+ let(:base_params) { { iids: [archived_issue.iid.to_s] } }
+
+ it 'excludes issues from archived projects' do
+ post_query
+
+ issue_ids = graphql_dig_at(graphql_data_at('issues', 'nodes'), :id)
+
+ expect(issue_ids).not_to include(archived_issue.to_gid.to_s)
+ end
+
+ context 'when includeArchived is true' do
+ let(:issue_filter_params) { { include_archived: true } }
+
+ it 'includes issues from archived projects' do
+ post_query
+
+ issue_ids = graphql_dig_at(graphql_data_at('issues', 'nodes'), :id)
+
+ expect(issue_ids).to include(archived_issue.to_gid.to_s)
+ end
+ end
+ end
+
+ it 'excludes issues from archived projects' do
+ post_query
+
+ issue_ids = graphql_dig_at(graphql_data_at('issues', 'nodes'), :id)
+
+ expect(issue_ids).not_to include(archived_issue.to_gid.to_s)
+ end
+
context 'when no filters are provided' do
let(:all_query_params) { {} }
diff --git a/spec/requests/api/graphql/jobs_query_spec.rb b/spec/requests/api/graphql/jobs_query_spec.rb
index 0aea8e4c253..7607aeac6e0 100644
--- a/spec/requests/api/graphql/jobs_query_spec.rb
+++ b/spec/requests/api/graphql/jobs_query_spec.rb
@@ -5,17 +5,26 @@ require 'spec_helper'
RSpec.describe 'getting job information', feature_category: :continuous_integration do
include GraphqlHelpers
- let_it_be(:job) { create(:ci_build, :success, name: 'job1') }
-
let(:query) do
- graphql_query_for(:jobs)
+ graphql_query_for(
+ :jobs, {}, %(
+ count
+ nodes {
+ #{all_graphql_fields_for(::Types::Ci::JobType, max_depth: 1)}
+ })
+ )
end
+ let_it_be(:runner) { create(:ci_runner) }
+ let_it_be(:job) { create(:ci_build, :success, name: 'job1', runner: runner) }
+
+ subject(:request) { post_graphql(query, current_user: current_user) }
+
context 'when user is admin' do
let_it_be(:current_user) { create(:admin) }
- it 'has full access to all jobs', :aggregate_failure do
- post_graphql(query, current_user: current_user)
+ it 'has full access to all jobs', :aggregate_failures do
+ request
expect(graphql_data_at(:jobs, :count)).to eq(1)
expect(graphql_data_at(:jobs, :nodes)).to contain_exactly(a_graphql_entity_for(job))
@@ -25,14 +34,14 @@ RSpec.describe 'getting job information', feature_category: :continuous_integrat
let_it_be(:pending_job) { create(:ci_build, :pending) }
let_it_be(:failed_job) { create(:ci_build, :failed) }
- it 'gets pending jobs', :aggregate_failure do
+ it 'gets pending jobs', :aggregate_failures do
post_graphql(graphql_query_for(:jobs, { statuses: :PENDING }), current_user: current_user)
expect(graphql_data_at(:jobs, :count)).to eq(1)
expect(graphql_data_at(:jobs, :nodes)).to contain_exactly(a_graphql_entity_for(pending_job))
end
- it 'gets pending and failed jobs', :aggregate_failure do
+ it 'gets pending and failed jobs', :aggregate_failures do
post_graphql(graphql_query_for(:jobs, { statuses: [:PENDING, :FAILED] }), current_user: current_user)
expect(graphql_data_at(:jobs, :count)).to eq(2)
@@ -40,13 +49,27 @@ RSpec.describe 'getting job information', feature_category: :continuous_integrat
a_graphql_entity_for(failed_job)])
end
end
+
+ context 'when N+1 queries' do
+ it 'avoids N+1 queries successfully', :use_sql_query_cache do
+ post_graphql(query, current_user: current_user) # warmup
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: current_user)
+ end
+
+ create(:ci_build, :success, name: 'job2', runner: create(:ci_runner))
+
+ expect { post_graphql(query, current_user: current_user) }.not_to exceed_all_query_limit(control)
+ end
+ end
end
context 'if the user is not an admin' do
let_it_be(:current_user) { create(:user) }
- it 'has no access to the jobs', :aggregate_failure do
- post_graphql(query, current_user: current_user)
+ it 'has no access to the jobs', :aggregate_failures do
+ request
expect(graphql_data_at(:jobs, :count)).to eq(0)
expect(graphql_data_at(:jobs, :nodes)).to match_array([])
diff --git a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
index 4dd47142c40..143bc1672f8 100644
--- a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
+++ b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
@@ -22,6 +22,7 @@ RSpec.describe 'Getting Metrics Dashboard Annotations', feature_category: :metri
create(:metrics_dashboard_annotation, environment: environment, starting_at: to.advance(minutes: 5), dashboard_path: path)
end
+ let(:remove_monitor_metrics) { false }
let(:args) { "from: \"#{from}\", to: \"#{to}\"" }
let(:fields) do
<<~QUERY
@@ -50,6 +51,7 @@ RSpec.describe 'Getting Metrics Dashboard Annotations', feature_category: :metri
end
before do
+ stub_feature_flags(remove_monitor_metrics: remove_monitor_metrics)
project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end
@@ -85,4 +87,18 @@ RSpec.describe 'Getting Metrics Dashboard Annotations', feature_category: :metri
it_behaves_like 'a working graphql query'
end
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ let(:remove_monitor_metrics) { true }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns nil' do
+ annotations = graphql_data.dig(
+ 'project', 'environments', 'nodes', 0, 'metricsDashboard', 'annotations'
+ )
+
+ expect(annotations).to be_nil
+ end
+ end
end
diff --git a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
index 8db0844c6d7..b7d9b59f5fe 100644
--- a/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
+++ b/spec/requests/api/graphql/metrics/dashboard_query_spec.rb
@@ -45,7 +45,10 @@ RSpec.describe 'Getting Metrics Dashboard', feature_category: :metrics do
end
context 'for user with developer access' do
+ let(:remove_monitor_metrics) { false }
+
before do
+ stub_feature_flags(remove_monitor_metrics: remove_monitor_metrics)
project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end
@@ -82,6 +85,18 @@ RSpec.describe 'Getting Metrics Dashboard', feature_category: :metrics do
expect(dashboard).to eql("path" => path, "schemaValidationWarnings" => ["dashboard: can't be blank", "panel_groups: should be an array of panel_groups objects"])
end
end
+
+ context 'metrics dashboard feature is unavailable' do
+ let(:remove_monitor_metrics) { true }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns nil' do
+ dashboard = graphql_data.dig('project', 'environments', 'nodes', 0, 'metricsDashboard')
+
+ expect(dashboard).to be_nil
+ end
+ end
end
context 'requested dashboard can not be found' do
diff --git a/spec/requests/api/graphql/multiplexed_queries_spec.rb b/spec/requests/api/graphql/multiplexed_queries_spec.rb
index 4d615d3eaa4..0a5c87ebef8 100644
--- a/spec/requests/api/graphql/multiplexed_queries_spec.rb
+++ b/spec/requests/api/graphql/multiplexed_queries_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Multiplexed queries', feature_category: :not_owned do
+RSpec.describe 'Multiplexed queries', feature_category: :shared do
include GraphqlHelpers
it 'returns responses for multiple queries' do
diff --git a/spec/requests/api/graphql/mutations/achievements/award_spec.rb b/spec/requests/api/graphql/mutations/achievements/award_spec.rb
new file mode 100644
index 00000000000..9bc0751e924
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/achievements/award_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Achievements::Award, feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:recipient) { create(:user) }
+
+ let(:mutation) { graphql_mutation(:achievements_award, params) }
+ let(:achievement_id) { achievement&.to_global_id }
+ let(:recipient_id) { recipient&.to_global_id }
+ let(:params) do
+ {
+ achievement_id: achievement_id,
+ user_id: recipient_id
+ }
+ end
+
+ subject { post_graphql_mutation(mutation, current_user: current_user) }
+
+ def mutation_response
+ graphql_mutation_response(:achievements_create)
+ end
+
+ before_all do
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { developer }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not create an achievement' do
+ expect { subject }.not_to change { Achievements::UserAchievement.count }
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { maintainer }
+
+ context 'when the params are invalid' do
+ let(:achievement) { nil }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s).to include('invalid value for achievementId (Expected value to not be null)')
+ end
+ end
+
+ context 'when the recipient_id is invalid' do
+ let(:recipient_id) { "gid://gitlab/User/#{non_existing_record_id}" }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_data_at(:achievements_award,
+ :errors)).to include("Couldn't find User with 'id'=#{non_existing_record_id}")
+ end
+ end
+
+ context 'when the achievement_id is invalid' do
+ let(:achievement_id) { "gid://gitlab/Achievements::Achievement/#{non_existing_record_id}" }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ end
+
+ it 'returns the relevant error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ it 'creates an achievement' do
+ expect { subject }.to change { Achievements::UserAchievement.count }.by(1)
+ end
+
+ it 'returns the new achievement' do
+ subject
+
+ expect(graphql_data_at(:achievements_award, :user_achievement, :achievement, :id))
+ .to eq(achievement.to_global_id.to_s)
+ expect(graphql_data_at(:achievements_award, :user_achievement, :user, :id))
+ .to eq(recipient.to_global_id.to_s)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/achievements/delete_spec.rb b/spec/requests/api/graphql/mutations/achievements/delete_spec.rb
new file mode 100644
index 00000000000..276da4f46a8
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/achievements/delete_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Achievements::Delete, feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ let!(:achievement) { create(:achievement, namespace: group) }
+ let(:mutation) { graphql_mutation(:achievements_delete, params) }
+ let(:achievement_id) { achievement&.to_global_id }
+ let(:params) { { achievement_id: achievement_id } }
+
+ subject { post_graphql_mutation(mutation, current_user: current_user) }
+
+ def mutation_response
+ graphql_mutation_response(:achievements_delete)
+ end
+
+ before_all do
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { developer }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not revoke any achievements' do
+ expect { subject }.not_to change { Achievements::Achievement.count }
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { maintainer }
+
+ context 'when the params are invalid' do
+ let(:achievement) { nil }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s).to include('invalid value for achievementId (Expected value to not be null)')
+ end
+ end
+
+ context 'when the achievement_id is invalid' do
+ let(:achievement_id) { "gid://gitlab/Achievements::Achievement/#{non_existing_record_id}" }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ end
+
+ it 'returns the relevant error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ it 'deletes the achievement' do
+ expect { subject }.to change { Achievements::Achievement.count }.by(-1)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/achievements/revoke_spec.rb b/spec/requests/api/graphql/mutations/achievements/revoke_spec.rb
new file mode 100644
index 00000000000..925a1bb9fcc
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/achievements/revoke_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Achievements::Revoke, feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:user_achievement) { create(:user_achievement, achievement: achievement) }
+
+ let(:mutation) { graphql_mutation(:achievements_revoke, params) }
+ let(:user_achievement_id) { user_achievement&.to_global_id }
+ let(:params) { { user_achievement_id: user_achievement_id } }
+
+ subject { post_graphql_mutation(mutation, current_user: current_user) }
+
+ def mutation_response
+ graphql_mutation_response(:achievements_create)
+ end
+
+ before_all do
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { developer }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not revoke any achievements' do
+ expect { subject }.not_to change { Achievements::UserAchievement.where(revoked_by_user_id: nil).count }
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { maintainer }
+
+ context 'when the params are invalid' do
+ let(:user_achievement) { nil }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s).to include('invalid value for userAchievementId (Expected value to not be null)')
+ end
+ end
+
+ context 'when the user_achievement_id is invalid' do
+ let(:user_achievement_id) { "gid://gitlab/Achievements::UserAchievement/#{non_existing_record_id}" }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ end
+
+ it 'returns the relevant error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ it 'revokes an achievement' do
+ expect { subject }.to change { Achievements::UserAchievement.where(revoked_by_user_id: nil).count }.by(-1)
+ end
+
+ it 'returns the revoked achievement' do
+ subject
+
+ expect(graphql_data_at(:achievements_revoke, :user_achievement, :achievement, :id))
+ .to eq(achievement.to_global_id.to_s)
+ expect(graphql_data_at(:achievements_revoke, :user_achievement, :revoked_by_user, :id))
+ .to eq(current_user.to_global_id.to_s)
+ expect(graphql_data_at(:achievements_revoke, :user_achievement, :revoked_at))
+ .not_to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/achievements/update_spec.rb b/spec/requests/api/graphql/mutations/achievements/update_spec.rb
new file mode 100644
index 00000000000..b2bb01b564c
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/achievements/update_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Achievements::Update, feature_category: :user_profile do
+ include GraphqlHelpers
+ include WorkhorseHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ let!(:achievement) { create(:achievement, namespace: group) }
+ let(:mutation) { graphql_mutation(:achievements_update, params) }
+ let(:achievement_id) { achievement&.to_global_id }
+ let(:params) { { achievement_id: achievement_id, name: 'GitLab', avatar: avatar } }
+ let(:avatar) { nil }
+
+ subject { post_graphql_mutation_with_uploads(mutation, current_user: current_user) }
+
+ def mutation_response
+ graphql_mutation_response(:achievements_update)
+ end
+
+ before_all do
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ end
+
+ context 'when the user does not have permission' do
+ let(:current_user) { developer }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not update the achievement' do
+ expect { subject }.not_to change { achievement.reload.name }
+ end
+ end
+
+ context 'when the user has permission' do
+ let(:current_user) { maintainer }
+
+ context 'when the params are invalid' do
+ let(:achievement) { nil }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s).to include('invalid value for achievementId (Expected value to not be null)')
+ end
+ end
+
+ context 'when the achievement_id is invalid' do
+ let(:achievement_id) { "gid://gitlab/Achievements::Achievement/#{non_existing_record_id}" }
+
+ it 'returns the validation error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ end
+
+ it 'returns the relevant permission error' do
+ subject
+
+ expect(graphql_errors.to_s)
+ .to include("The resource that you are attempting to access does not exist or you don't have permission")
+ end
+ end
+
+ context 'with a new avatar' do
+ let(:avatar) { fixture_file_upload("spec/fixtures/dk.png") }
+
+ it 'updates the achievement' do
+ subject
+
+ achievement.reload
+
+ expect(achievement.name).to eq('GitLab')
+ expect(achievement.avatar.file).not_to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
index 64ea6d32f5f..b3d25155a6f 100644
--- a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
+++ b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_category: :not_owned do
+RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues, feature_category: :shared do
include GraphqlHelpers
let_it_be(:admin) { create(:admin) }
diff --git a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
index fdbff0f93cd..18cc85d36e0 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Adding an AwardEmoji', feature_category: :not_owned do
+RSpec.describe 'Adding an AwardEmoji', feature_category: :shared do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
index e200bfc2d18..7ec2b061a88 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Removing an AwardEmoji', feature_category: :not_owned do
+RSpec.describe 'Removing an AwardEmoji', feature_category: :shared do
include GraphqlHelpers
let(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
index 6dba2b58357..7c6a487cdd0 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Toggling an AwardEmoji', feature_category: :not_owned do
+RSpec.describe 'Toggling an AwardEmoji', feature_category: :shared do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb b/spec/requests/api/graphql/mutations/ci/job/cancel_spec.rb
index 468a9e57f56..abad1ae0812 100644
--- a/spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job/cancel_spec.rb
@@ -15,12 +15,12 @@ RSpec.describe "JobCancel", feature_category: :continuous_integration do
id: job.to_global_id.to_s
}
graphql_mutation(:job_cancel, variables,
- <<-QL
+ <<-QL
errors
job {
id
}
- QL
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/ci/job_play_spec.rb b/spec/requests/api/graphql/mutations/ci/job/play_spec.rb
index 9ba80e51dee..0c700248f85 100644
--- a/spec/requests/api/graphql/mutations/ci/job_play_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job/play_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'JobPlay', feature_category: :continuous_integration do
let(:mutation) do
graphql_mutation(:job_play, variables,
- <<-QL
+ <<-QL
errors
job {
id
@@ -28,7 +28,7 @@ RSpec.describe 'JobPlay', feature_category: :continuous_integration do
}
}
}
- QL
+ QL
)
end
@@ -63,7 +63,7 @@ RSpec.describe 'JobPlay', feature_category: :continuous_integration do
}
end
- it 'provides those variables to the job', :aggregated_errors do
+ it 'provides those variables to the job', :aggregate_failures do
expect_next_instance_of(Ci::PlayBuildService) do |instance|
expect(instance).to receive(:execute).with(an_instance_of(Ci::Build), variables[:variables]).and_call_original
end
diff --git a/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb b/spec/requests/api/graphql/mutations/ci/job/retry_spec.rb
index e49ee6f3163..4114c77491b 100644
--- a/spec/requests/api/graphql/mutations/ci/job_retry_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job/retry_spec.rb
@@ -16,12 +16,12 @@ RSpec.describe 'JobRetry', feature_category: :continuous_integration do
id: job.to_global_id.to_s
}
graphql_mutation(:job_retry, variables,
- <<-QL
+ <<-QL
errors
job {
id
}
- QL
+ QL
)
end
@@ -57,12 +57,12 @@ RSpec.describe 'JobRetry', feature_category: :continuous_integration do
}
graphql_mutation(:job_retry, variables,
- <<-QL
+ <<-QL
errors
job {
id
}
- QL
+ QL
)
end
diff --git a/spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb b/spec/requests/api/graphql/mutations/ci/job/unschedule_spec.rb
index 6868b0ea279..08e155e808b 100644
--- a/spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job/unschedule_spec.rb
@@ -15,7 +15,7 @@ RSpec.describe 'JobUnschedule', feature_category: :continuous_integration do
id: job.to_global_id.to_s
}
graphql_mutation(:job_unschedule, variables,
- <<-QL
+ <<-QL
errors
job {
id
diff --git a/spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb b/spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb
new file mode 100644
index 00000000000..4e25669a0ca
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/job_artifact/bulk_destroy_spec.rb
@@ -0,0 +1,197 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'BulkDestroy', feature_category: :build_artifacts do
+ include GraphqlHelpers
+
+ let(:maintainer) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:first_artifact) { create(:ci_job_artifact) }
+ let(:second_artifact) { create(:ci_job_artifact, project: project) }
+ let(:second_artifact_another_project) { create(:ci_job_artifact) }
+ let(:project) { first_artifact.job.project }
+ let(:ids) { [first_artifact.to_global_id.to_s] }
+ let(:not_authorized_project_error_message) do
+ "The resource that you are attempting to access " \
+ "does not exist or you don't have permission to perform this action"
+ end
+
+ let(:mutation) do
+ variables = {
+ project_id: project.to_global_id.to_s,
+ ids: ids
+ }
+ graphql_mutation(:bulk_destroy_job_artifacts, variables, <<~FIELDS)
+ destroyedCount
+ destroyedIds
+ errors
+ FIELDS
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:bulk_destroy_job_artifacts) }
+
+ it 'fails to destroy the artifact if a user not in a project' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect(graphql_errors).to include(
+ a_hash_including('message' => not_authorized_project_error_message)
+ )
+
+ expect(first_artifact.reload).to be_persisted
+ end
+
+ context 'when the `ci_job_artifact_bulk_destroy` feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_job_artifact_bulk_destroy: false)
+ project.add_maintainer(maintainer)
+ end
+
+ it 'returns a resource not available error' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect(graphql_errors).to contain_exactly(
+ hash_including(
+ 'message' => '`ci_job_artifact_bulk_destroy` feature flag is disabled.'
+ )
+ )
+ end
+ end
+
+ context "when the user is a developer in a project" do
+ before do
+ project.add_developer(developer)
+ end
+
+ it 'fails to destroy the artifact' do
+ post_graphql_mutation(mutation, current_user: developer)
+
+ expect(graphql_errors).to include(
+ a_hash_including('message' => not_authorized_project_error_message)
+ )
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(first_artifact.reload).to be_persisted
+ end
+ end
+
+ context "when the user is a maintainer in a project" do
+ before do
+ project.add_maintainer(maintainer)
+ end
+
+ shared_examples 'failing mutation' do
+ it 'rejects the request' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect(graphql_errors(mutation_response)).to include(expected_error_message)
+
+ expected_not_found_artifacts.each do |artifact|
+ expect { artifact.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ expected_found_artifacts.each do |artifact|
+ expect(artifact.reload).to be_persisted
+ end
+ end
+ end
+
+ it 'destroys the artifact' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect(mutation_response).to include("destroyedCount" => 1, "destroyedIds" => [gid_string(first_artifact)])
+ expect(response).to have_gitlab_http_status(:success)
+ expect { first_artifact.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ context "and one artifact doesn't belong to the project" do
+ let(:not_owned_artifact) { create(:ci_job_artifact) }
+ let(:ids) { [first_artifact.to_global_id.to_s, not_owned_artifact.to_global_id.to_s] }
+ let(:expected_error_message) { "Not all artifacts belong to requested project" }
+ let(:expected_not_found_artifacts) { [] }
+ let(:expected_found_artifacts) { [first_artifact, not_owned_artifact] }
+
+ it_behaves_like 'failing mutation'
+ end
+
+ context "and multiple artifacts belong to the maintainer's project" do
+ let(:ids) { [first_artifact.to_global_id.to_s, second_artifact.to_global_id.to_s] }
+
+ it 'destroys all artifacts' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect(mutation_response).to include(
+ "destroyedCount" => 2,
+ "destroyedIds" => [gid_string(first_artifact), gid_string(second_artifact)]
+ )
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect { first_artifact.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { second_artifact.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context "and one artifact belongs to a different maintainer's project" do
+ let(:ids) { [first_artifact.to_global_id.to_s, second_artifact_another_project.to_global_id.to_s] }
+ let(:expected_found_artifacts) { [first_artifact, second_artifact_another_project] }
+ let(:expected_not_found_artifacts) { [] }
+ let(:expected_error_message) { "Not all artifacts belong to requested project" }
+
+ it_behaves_like 'failing mutation'
+ end
+
+ context "and not found" do
+ let(:ids) { [first_artifact.to_global_id.to_s, second_artifact.to_global_id.to_s] }
+ let(:not_found_ids) { expected_not_found_artifacts.map(&:id).join(',') }
+ let(:expected_error_message) { "Artifacts (#{not_found_ids}) not found" }
+
+ before do
+ expected_not_found_artifacts.each(&:destroy!)
+ end
+
+ context "with one artifact" do
+ let(:expected_not_found_artifacts) { [second_artifact] }
+ let(:expected_found_artifacts) { [first_artifact] }
+
+ it_behaves_like 'failing mutation'
+ end
+
+ context "with all artifact" do
+ let(:expected_not_found_artifacts) { [first_artifact, second_artifact] }
+ let(:expected_found_artifacts) { [] }
+
+ it_behaves_like 'failing mutation'
+ end
+ end
+
+ context 'when empty request' do
+ before do
+ project.add_maintainer(maintainer)
+ end
+
+ context 'with nil value' do
+ let(:ids) { nil }
+
+ it 'does nothing and returns empty answer' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect_graphql_errors_to_include(/was provided invalid value for ids \(Expected value to not be null\)/)
+ end
+ end
+
+ context 'with empty array' do
+ let(:ids) { [] }
+
+ it 'raises argument error' do
+ post_graphql_mutation(mutation, current_user: maintainer)
+
+ expect_graphql_errors_to_include(/IDs array of job artifacts can not be empty/)
+ end
+ end
+ end
+
+ def gid_string(object)
+ Gitlab::GlobalId.build(object, id: object.id).to_s
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
index 55e728b2141..8791d793cb4 100644
--- a/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/job_token_scope/add_project_spec.rb
@@ -53,14 +53,29 @@ RSpec.describe 'CiJobTokenScopeAddProject', feature_category: :continuous_integr
before do
target_project.add_developer(current_user)
+ stub_feature_flags(frozen_outbound_job_token_scopes_override: false)
end
- it 'adds the target project to the job token scope' do
+ it 'adds the target project to the inbound job token scope' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty
- end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1)
+ end.to change { Ci::JobToken::ProjectScopeLink.inbound.count }.by(1)
+ end
+
+ context 'when FF frozen_outbound_job_token_scopes is disabled' do
+ before do
+ stub_feature_flags(frozen_outbound_job_token_scopes: false)
+ end
+
+ it 'adds the target project to the outbound job token scope' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty
+ end.to change { Ci::JobToken::ProjectScopeLink.outbound.count }.by(1)
+ end
end
context 'when invalid target project is provided' do
diff --git a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
index 99e55c44773..aa00069b241 100644
--- a/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/ci/project_ci_cd_settings_update_spec.rb
@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integration do
include GraphqlHelpers
+ before do
+ stub_feature_flags(frozen_outbound_job_token_scopes_override: false)
+ end
+
let_it_be(:project) do
create(:project,
keep_latest_artifact: true,
@@ -18,12 +22,11 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr
full_path: project.full_path,
keep_latest_artifact: false,
job_token_scope_enabled: false,
- inbound_job_token_scope_enabled: false,
- opt_in_jwt: true
+ inbound_job_token_scope_enabled: false
}
end
- let(:mutation) { graphql_mutation(:ci_cd_settings_update, variables) }
+ let(:mutation) { graphql_mutation(:project_ci_cd_settings_update, variables) }
context 'when unauthorized' do
let(:user) { create(:user) }
@@ -61,7 +64,36 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr
expect(project.keep_latest_artifact).to eq(false)
end
- it 'updates job_token_scope_enabled' do
+ describe 'ci_cd_settings_update deprecated mutation' do
+ let(:mutation) { graphql_mutation(:ci_cd_settings_update, variables) }
+
+ it 'returns error' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_errors).to(
+ include(
+ hash_including('message' => '`remove_cicd_settings_update` feature flag is enabled.')
+ )
+ )
+ end
+
+ context 'when remove_cicd_settings_update FF is disabled' do
+ before do
+ stub_feature_flags(remove_cicd_settings_update: false)
+ end
+
+ it 'updates ci cd settings' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ project.reload
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(project.keep_latest_artifact).to eq(false)
+ end
+ end
+ end
+
+ it 'allows setting job_token_scope_enabled to false' do
post_graphql_mutation(mutation, current_user: user)
project.reload
@@ -70,6 +102,50 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr
expect(project.ci_outbound_job_token_scope_enabled).to eq(false)
end
+ context 'when job_token_scope_enabled: true' do
+ let(:variables) do
+ {
+ full_path: project.full_path,
+ keep_latest_artifact: false,
+ job_token_scope_enabled: true,
+ inbound_job_token_scope_enabled: false
+ }
+ end
+
+ it 'prevents the update', :aggregate_failures do
+ project.update!(ci_outbound_job_token_scope_enabled: false)
+ post_graphql_mutation(mutation, current_user: user)
+
+ project.reload
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(graphql_errors).to(
+ include(
+ hash_including(
+ 'message' => 'job_token_scope_enabled can only be set to false'
+ )
+ )
+ )
+ expect(project.ci_outbound_job_token_scope_enabled).to eq(false)
+ end
+ end
+
+ context 'when FF frozen_outbound_job_token_scopes is disabled' do
+ before do
+ stub_feature_flags(frozen_outbound_job_token_scopes: false)
+ end
+
+ it 'allows setting job_token_scope_enabled to true' do
+ project.update!(ci_outbound_job_token_scope_enabled: true)
+ post_graphql_mutation(mutation, current_user: user)
+
+ project.reload
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(project.ci_outbound_job_token_scope_enabled).to eq(false)
+ end
+ end
+
it 'does not update job_token_scope_enabled if not specified' do
variables.except!(:job_token_scope_enabled)
@@ -101,30 +177,6 @@ RSpec.describe 'ProjectCiCdSettingsUpdate', feature_category: :continuous_integr
expect(response).to have_gitlab_http_status(:success)
expect(project.ci_inbound_job_token_scope_enabled).to eq(true)
end
-
- context 'when ci_inbound_job_token_scope disabled' do
- before do
- stub_feature_flags(ci_inbound_job_token_scope: false)
- end
-
- it 'does not update inbound_job_token_scope_enabled' do
- post_graphql_mutation(mutation, current_user: user)
-
- project.reload
-
- expect(response).to have_gitlab_http_status(:success)
- expect(project.ci_inbound_job_token_scope_enabled).to eq(true)
- end
- end
- end
-
- it 'updates ci_opt_in_jwt' do
- post_graphql_mutation(mutation, current_user: user)
-
- project.reload
-
- expect(response).to have_gitlab_http_status(:success)
- expect(project.ci_opt_in_jwt).to eq(true)
end
context 'when bad arguments are provided' do
diff --git a/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb b/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb
new file mode 100644
index 00000000000..1658c277ed0
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/runner/create_spec.rb
@@ -0,0 +1,313 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'RunnerCreate', feature_category: :runner_fleet do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group_owner) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:other_group) { create(:group) }
+
+ let(:mutation_params) do
+ {
+ description: 'create description',
+ maintenance_note: 'create maintenance note',
+ maximum_timeout: 900,
+ access_level: 'REF_PROTECTED',
+ paused: true,
+ run_untagged: false,
+ tag_list: %w[tag1 tag2]
+ }.deep_merge(mutation_scope_params)
+ end
+
+ let(:mutation) do
+ variables = {
+ **mutation_params
+ }
+
+ graphql_mutation(
+ :runner_create,
+ variables,
+ <<-QL
+ runner {
+ ephemeralAuthenticationToken
+
+ runnerType
+ description
+ maintenanceNote
+ paused
+ tagList
+ accessLevel
+ locked
+ maximumTimeout
+ runUntagged
+ }
+ errors
+ QL
+ )
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:runner_create) }
+
+ before do
+ group.add_owner(group_owner)
+ end
+
+ shared_context 'when model is invalid returns error' do
+ let(:mutation_params) do
+ {
+ description: '',
+ maintenanceNote: '',
+ paused: true,
+ accessLevel: 'NOT_PROTECTED',
+ runUntagged: false,
+ tagList: [],
+ maximumTimeout: 1
+ }.deep_merge(mutation_scope_params)
+ end
+
+ it do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+
+ expect(mutation_response['errors']).to contain_exactly(
+ 'Tags list can not be empty when runner is not allowed to pick untagged jobs',
+ 'Maximum timeout needs to be at least 10 minutes'
+ )
+ end
+ end
+
+ shared_context 'when user does not have permissions' do
+ let(:current_user) { user }
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(
+ 'The resource that you are attempting to access does not exist ' \
+ "or you don't have permission to perform this action"
+ )
+ end
+ end
+
+ shared_context 'when :create_runner_workflow_for_namespace feature flag is disabled' do
+ before do
+ stub_feature_flags(create_runner_workflow_for_namespace: [other_group])
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include('`create_runner_workflow_for_namespace` feature flag is disabled.')
+ end
+ end
+
+ shared_examples 'when runner is created successfully' do
+ it do
+ expected_args = { user: current_user, params: anything }
+ expect_next_instance_of(::Ci::Runners::CreateRunnerService, expected_args) do |service|
+ expect(service).to receive(:execute).and_call_original
+ end
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+
+ expect(mutation_response['errors']).to eq([])
+ expect(mutation_response['runner']).not_to be_nil
+ mutation_params.except(:group_id, :project_id).each_key do |key|
+ expect(mutation_response['runner'][key.to_s.camelize(:lower)]).to eq mutation_params[key]
+ end
+
+ expect(mutation_response['runner']['ephemeralAuthenticationToken'])
+ .to start_with Ci::Runner::CREATED_RUNNER_TOKEN_PREFIX
+ end
+ end
+
+ context 'when runnerType is INSTANCE_TYPE' do
+ let(:mutation_scope_params) do
+ { runner_type: 'INSTANCE_TYPE' }
+ end
+
+ it_behaves_like 'when user does not have permissions'
+
+ context 'when user has permissions', :enable_admin_mode do
+ let(:current_user) { admin }
+
+ context 'when :create_runner_workflow_for_admin feature flag is disabled' do
+ before do
+ stub_feature_flags(create_runner_workflow_for_admin: false)
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include('`create_runner_workflow_for_admin` feature flag is disabled.')
+ end
+ end
+
+ it_behaves_like 'when runner is created successfully'
+ it_behaves_like 'when model is invalid returns error'
+ end
+ end
+
+ context 'when runnerType is GROUP_TYPE' do
+ let(:mutation_scope_params) do
+ {
+ runner_type: 'GROUP_TYPE',
+ group_id: group.to_global_id
+ }
+ end
+
+ before do
+ stub_feature_flags(create_runner_workflow_for_namespace: [group])
+ end
+
+ it_behaves_like 'when user does not have permissions'
+
+ context 'when user has permissions' do
+ context 'when user is group owner' do
+ let(:current_user) { group_owner }
+
+ it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
+ it_behaves_like 'when runner is created successfully'
+ it_behaves_like 'when model is invalid returns error'
+
+ context 'when group_id is missing' do
+ let(:mutation_scope_params) do
+ { runner_type: 'GROUP_TYPE' }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include('`group_id` is missing')
+ end
+ end
+
+ context 'when group_id is malformed' do
+ let(:mutation_scope_params) do
+ {
+ runner_type: 'GROUP_TYPE',
+ group_id: ''
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(
+ "RunnerCreateInput! was provided invalid value for groupId"
+ )
+ end
+ end
+
+ context 'when group_id does not exist' do
+ let(:mutation_scope_params) do
+ {
+ runner_type: 'GROUP_TYPE',
+ group_id: "gid://gitlab/Group/#{non_existing_record_id}"
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(flattened_errors).not_to be_empty
+ end
+ end
+ end
+
+ context 'when user is admin in admin mode', :enable_admin_mode do
+ let(:current_user) { admin }
+
+ it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
+ it_behaves_like 'when runner is created successfully'
+ it_behaves_like 'when model is invalid returns error'
+ end
+ end
+ end
+
+ context 'when runnerType is PROJECT_TYPE' do
+ let_it_be(:project) { create(:project, namespace: group) }
+
+ let(:mutation_scope_params) do
+ {
+ runner_type: 'PROJECT_TYPE',
+ project_id: project.to_global_id
+ }
+ end
+
+ it_behaves_like 'when user does not have permissions'
+
+ context 'when user has permissions' do
+ context 'when user is group owner' do
+ let(:current_user) { group_owner }
+
+ it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
+ it_behaves_like 'when runner is created successfully'
+ it_behaves_like 'when model is invalid returns error'
+
+ context 'when project_id is missing' do
+ let(:mutation_scope_params) do
+ { runner_type: 'PROJECT_TYPE' }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include('`project_id` is missing')
+ end
+ end
+
+ context 'when project_id is malformed' do
+ let(:mutation_scope_params) do
+ {
+ runner_type: 'PROJECT_TYPE',
+ project_id: ''
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(
+ "RunnerCreateInput! was provided invalid value for projectId"
+ )
+ end
+ end
+
+ context 'when project_id does not exist' do
+ let(:mutation_scope_params) do
+ {
+ runner_type: 'PROJECT_TYPE',
+ project_id: "gid://gitlab/Project/#{non_existing_record_id}"
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(
+ 'The resource that you are attempting to access does not exist ' \
+ "or you don't have permission to perform this action"
+ )
+ end
+ end
+ end
+
+ context 'when user is admin in admin mode', :enable_admin_mode do
+ let(:current_user) { admin }
+
+ it_behaves_like 'when :create_runner_workflow_for_namespace feature flag is disabled'
+ it_behaves_like 'when runner is created successfully'
+ it_behaves_like 'when model is invalid returns error'
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb b/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb
index f544cef8864..ef0d44395bf 100644
--- a/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create a new cluster agent token', feature_category: :kubernetes_management do
+RSpec.describe 'Create a new cluster agent token', feature_category: :deployment_management do
include GraphqlHelpers
let_it_be(:cluster_agent) { create(:cluster_agent) }
diff --git a/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb b/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb
index 66e6c5cc629..1d1e72dcff9 100644
--- a/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Create a new cluster agent', feature_category: :kubernetes_management do
+RSpec.describe 'Create a new cluster agent', feature_category: :deployment_management do
include GraphqlHelpers
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb b/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb
index 27a566dfb8c..b70a6282a7a 100644
--- a/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Delete a cluster agent', feature_category: :kubernetes_management do
+RSpec.describe 'Delete a cluster agent', feature_category: :deployment_management do
include GraphqlHelpers
let(:cluster_agent) { create(:cluster_agent) }
diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
index 8b76c19cda6..ef159e41d3d 100644
--- a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe 'Destroying a container repository', feature_category: :container
expect(DeleteContainerRepositoryWorker)
.not_to receive(:perform_async)
- expect { subject }.to change { ::Packages::Event.count }.by(1)
+ subject
expect(container_repository_mutation_response).to match_schema('graphql/container_repository')
expect(container_repository_mutation_response['status']).to eq('DELETE_SCHEDULED')
@@ -53,7 +53,7 @@ RSpec.describe 'Destroying a container repository', feature_category: :container
expect(DeleteContainerRepositoryWorker)
.not_to receive(:perform_async).with(user.id, container_repository.id)
- expect { subject }.not_to change { ::Packages::Event.count }
+ subject
expect(mutation_response).to be_nil
end
diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb
index 9e07a831076..0cb607e13ec 100644
--- a/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb
+++ b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe 'Destroying a container repository tags', feature_category: :cont
it 'destroys the container repository tags' do
expect(Projects::ContainerRepository::DeleteTagsService)
.to receive(:new).and_call_original
- expect { subject }.to change { ::Packages::Event.count }.by(1)
+ subject
expect(tag_names_response).to eq(tags)
expect(errors_response).to eq([])
@@ -50,7 +50,7 @@ RSpec.describe 'Destroying a container repository tags', feature_category: :cont
expect(Projects::ContainerRepository::DeleteTagsService)
.not_to receive(:new)
- expect { subject }.not_to change { ::Packages::Event.count }
+ subject
expect(mutation_response).to be_nil
end
@@ -89,7 +89,7 @@ RSpec.describe 'Destroying a container repository tags', feature_category: :cont
let(:tags) { Array.new(Mutations::ContainerRepositories::DestroyTags::LIMIT + 1, 'x') }
it 'returns too many tags error' do
- expect { subject }.not_to change { ::Packages::Event.count }
+ subject
explanation = graphql_errors.dig(0, 'message')
expect(explanation).to eq(Mutations::ContainerRepositories::DestroyTags::TOO_MANY_TAGS_ERROR_MESSAGE)
@@ -113,7 +113,7 @@ RSpec.describe 'Destroying a container repository tags', feature_category: :cont
it 'does not create a package event' do
expect(::Packages::CreateEventService).not_to receive(:new)
- expect { subject }.not_to change { ::Packages::Event.count }
+ subject
end
end
end
diff --git a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
index ea2ce8a13e2..19a52086f34 100644
--- a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Creation of a new Custom Emoji', feature_category: :not_owned do
+RSpec.describe 'Creation of a new Custom Emoji', feature_category: :shared do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb
index ad7a043909a..2623d3d8410 100644
--- a/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/custom_emoji/destroy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Deletion of custom emoji', feature_category: :not_owned do
+RSpec.describe 'Deletion of custom emoji', feature_category: :shared do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
diff --git a/spec/requests/api/graphql/mutations/design_management/update_spec.rb b/spec/requests/api/graphql/mutations/design_management/update_spec.rb
new file mode 100644
index 00000000000..9558f2538f1
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/design_management/update_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe "updating designs", feature_category: :design_management do
+ include GraphqlHelpers
+ include DesignManagementTestHelpers
+
+ let_it_be(:issue) { create(:issue) }
+ let_it_be_with_reload(:design) { create(:design, description: 'old description', issue: issue) }
+ let_it_be(:developer) { create(:user, developer_projects: [issue.project]) }
+
+ let(:user) { developer }
+ let(:description) { 'new description' }
+
+ let(:mutation) do
+ input = {
+ id: design.to_global_id.to_s,
+ description: description
+ }.compact
+
+ graphql_mutation(:design_management_update, input, <<~FIELDS)
+ errors
+ design {
+ description
+ descriptionHtml
+ }
+ FIELDS
+ end
+
+ let(:update_design) { post_graphql_mutation(mutation, current_user: user) }
+ let(:mutation_response) { graphql_mutation_response(:design_management_update) }
+
+ before do
+ enable_design_management
+ end
+
+ it 'updates design' do
+ update_design
+
+ expect(graphql_errors).not_to be_present
+ expect(mutation_response).to eq(
+ 'errors' => [],
+ 'design' => {
+ 'description' => description,
+ 'descriptionHtml' => "<p data-sourcepos=\"1:1-1:15\" dir=\"auto\">#{description}</p>"
+ }
+ )
+ end
+
+ context 'when the user is not allowed to update designs' do
+ let(:user) { create(:user) }
+
+ it 'returns an error' do
+ update_design
+
+ expect(graphql_errors).to be_present
+ end
+ end
+
+ context 'when update fails' do
+ let(:description) { 'x' * 1_000_001 }
+
+ it 'returns an error' do
+ update_design
+
+ expect(graphql_errors).not_to be_present
+ expect(mutation_response).to eq(
+ 'errors' => ["Description is too long (maximum is 1000000 characters)"],
+ 'design' => {
+ 'description' => 'old description',
+ 'descriptionHtml' => '<p data-sourcepos="1:1-1:15" dir="auto">old description</p>'
+ }
+ )
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb b/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb
index b9c83311908..b729585a89b 100644
--- a/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/bulk_update_spec.rb
@@ -8,7 +8,9 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do
let_it_be(:developer) { create(:user) }
let_it_be(:group) { create(:group).tap { |group| group.add_developer(developer) } }
let_it_be(:project) { create(:project, group: group) }
- let_it_be(:updatable_issues, reload: true) { create_list(:issue, 2, project: project) }
+ let_it_be(:label1) { create(:group_label, group: group) }
+ let_it_be(:label2) { create(:group_label, group: group) }
+ let_it_be(:updatable_issues, reload: true) { create_list(:issue, 2, project: project, label_ids: [label1.id]) }
let_it_be(:milestone) { create(:milestone, group: group) }
let(:parent) { project }
@@ -21,10 +23,36 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do
let(:additional_arguments) do
{
assignee_ids: [current_user.to_gid.to_s],
- milestone_id: milestone.to_gid.to_s
+ milestone_id: milestone.to_gid.to_s,
+ state_event: :CLOSE,
+ add_label_ids: [label2.to_gid.to_s],
+ remove_label_ids: [label1.to_gid.to_s],
+ subscription_event: :UNSUBSCRIBE
}
end
+ before_all do
+ updatable_issues.each { |i| i.subscribe(developer, project) }
+ end
+
+ context 'when Gitlab is FOSS only' do
+ unless Gitlab.ee?
+ context 'when parent is a group' do
+ let(:parent) { group }
+
+ it 'does not allow bulk updating issues at the group level' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors).to contain_exactly(
+ hash_including(
+ 'message' => match(/does not represent an instance of IssueParent/)
+ )
+ )
+ end
+ end
+ end
+ end
+
context 'when the `bulk_update_issues_mutation` feature flag is disabled' do
before do
stub_feature_flags(bulk_update_issues_mutation: false)
@@ -67,6 +95,11 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do
updatable_issues.each(&:reload)
end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([]).to([current_user.id] * 2)
.and(change { updatable_issues.map(&:milestone_id) }.from([nil] * 2).to([milestone.id] * 2))
+ .and(change { updatable_issues.map(&:state) }.from(['opened'] * 2).to(['closed'] * 2))
+ .and(change { updatable_issues.flat_map(&:label_ids) }.from([label1.id] * 2).to([label2.id] * 2))
+ .and(
+ change { updatable_issues.map { |i| i.subscribed?(developer, project) } }.from([true] * 2).to([false] * 2)
+ )
expect(mutation_response).to include(
'updatedIssueCount' => updatable_issues.count
@@ -88,37 +121,6 @@ RSpec.describe 'Bulk update issues', feature_category: :team_planning do
end
end
- context 'when scoping to a parent group' do
- let(:parent) { group }
-
- it 'updates all issues' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- updatable_issues.each(&:reload)
- end.to change { updatable_issues.flat_map(&:assignee_ids) }.from([]).to([current_user.id] * 2)
- .and(change { updatable_issues.map(&:milestone_id) }.from([nil] * 2).to([milestone.id] * 2))
-
- expect(mutation_response).to include(
- 'updatedIssueCount' => updatable_issues.count
- )
- end
-
- context 'when current user cannot read the specified group' do
- let(:parent) { create(:group, :private) }
-
- it 'returns a resource not found error' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(graphql_errors).to contain_exactly(
- hash_including(
- 'message' => "The resource that you are attempting to access does not exist or you don't have " \
- 'permission to perform this action'
- )
- )
- end
- end
- end
-
context 'when setting arguments to null or none' do
let(:additional_arguments) { { assignee_ids: [], milestone_id: nil } }
diff --git a/spec/requests/api/graphql/mutations/issues/create_spec.rb b/spec/requests/api/graphql/mutations/issues/create_spec.rb
index d2d2f0014d6..b5a9c549045 100644
--- a/spec/requests/api/graphql/mutations/issues/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/create_spec.rb
@@ -66,7 +66,6 @@ RSpec.describe 'Create an issue', feature_category: :team_planning do
created_issue = Issue.last
expect(created_issue.work_item_type.base_type).to eq('task')
- expect(created_issue.issue_type).to eq('task')
end
end
diff --git a/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb b/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb
index ad70129a7bc..f15b52f53a3 100644
--- a/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb
+++ b/spec/requests/api/graphql/mutations/members/groups/bulk_update_spec.rb
@@ -5,126 +5,14 @@ require 'spec_helper'
RSpec.describe 'GroupMemberBulkUpdate', feature_category: :subgroups do
include GraphqlHelpers
- let_it_be(:current_user) { create(:user) }
- let_it_be(:user1) { create(:user) }
- let_it_be(:user2) { create(:user) }
- let_it_be(:group) { create(:group) }
- let_it_be(:group_member1) { create(:group_member, group: group, user: user1) }
- let_it_be(:group_member2) { create(:group_member, group: group, user: user2) }
+ let_it_be(:parent_group) { create(:group) }
+ let_it_be(:parent_group_member) { create(:group_member, group: parent_group) }
+ let_it_be(:group) { create(:group, parent: parent_group) }
+ let_it_be(:source) { group }
+ let_it_be(:member_type) { :group_member }
let_it_be(:mutation_name) { :group_member_bulk_update }
+ let_it_be(:source_id_key) { 'group_id' }
+ let_it_be(:response_member_field) { 'groupMembers' }
- let(:input) do
- {
- 'group_id' => group.to_global_id.to_s,
- 'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s],
- 'access_level' => 'GUEST'
- }
- end
-
- let(:extra_params) { { expires_at: 10.days.from_now } }
- let(:input_params) { input.merge(extra_params) }
- let(:mutation) { graphql_mutation(mutation_name, input_params) }
- let(:mutation_response) { graphql_mutation_response(mutation_name) }
-
- context 'when user is not logged-in' do
- it_behaves_like 'a mutation that returns a top-level access error'
- end
-
- context 'when user is not an owner' do
- before do
- group.add_maintainer(current_user)
- end
-
- it_behaves_like 'a mutation that returns a top-level access error'
- end
-
- context 'when user is an owner' do
- before do
- group.add_owner(current_user)
- end
-
- shared_examples 'updates the user access role' do
- specify do
- post_graphql_mutation(mutation, current_user: current_user)
-
- new_access_levels = mutation_response['groupMembers'].map { |member| member['accessLevel']['integerValue'] }
- expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response['errors']).to be_empty
- expect(new_access_levels).to all(be Gitlab::Access::GUEST)
- end
- end
-
- it_behaves_like 'updates the user access role'
-
- context 'when inherited members are passed' do
- let_it_be(:subgroup) { create(:group, parent: group) }
- let_it_be(:subgroup_member) { create(:group_member, group: subgroup) }
-
- let(:input) do
- {
- 'group_id' => group.to_global_id.to_s,
- 'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s, subgroup_member.user.to_global_id.to_s],
- 'access_level' => 'GUEST'
- }
- end
-
- it 'does not update the members' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- error = Mutations::Members::Groups::BulkUpdate::INVALID_MEMBERS_ERROR
- expect(json_response['errors'].first['message']).to include(error)
- end
- end
-
- context 'when members count is more than the allowed limit' do
- let(:max_members_update_limit) { 1 }
-
- before do
- stub_const('Mutations::Members::Groups::BulkUpdate::MAX_MEMBERS_UPDATE_LIMIT', max_members_update_limit)
- end
-
- it 'does not update the members' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- error = Mutations::Members::Groups::BulkUpdate::MAX_MEMBERS_UPDATE_ERROR
- expect(json_response['errors'].first['message']).to include(error)
- end
- end
-
- context 'when the update service raises access denied error' do
- before do
- allow_next_instance_of(Members::UpdateService) do |instance|
- allow(instance).to receive(:execute).and_raise(Gitlab::Access::AccessDeniedError)
- end
- end
-
- it 'does not update the members' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(mutation_response['groupMembers']).to be_nil
- expect(mutation_response['errors'])
- .to contain_exactly("Unable to update members, please check user permissions.")
- end
- end
-
- context 'when the update service returns an error message' do
- before do
- allow_next_instance_of(Members::UpdateService) do |instance|
- error_result = {
- message: 'Expires at cannot be a date in the past',
- status: :error,
- members: [group_member1]
- }
- allow(instance).to receive(:execute).and_return(error_result)
- end
- end
-
- it 'will pass through the error' do
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(mutation_response['groupMembers'].first['id']).to eq(group_member1.to_global_id.to_s)
- expect(mutation_response['errors']).to contain_exactly('Expires at cannot be a date in the past')
- end
- end
- end
+ it_behaves_like 'members bulk update mutation'
end
diff --git a/spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb b/spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb
new file mode 100644
index 00000000000..cbef9715cbe
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/members/projects/bulk_update_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'ProjectMemberBulkUpdate', feature_category: :projects do
+ include GraphqlHelpers
+
+ let_it_be(:parent_group) { create(:group) }
+ let_it_be(:parent_group_member) { create(:group_member, group: parent_group) }
+ let_it_be(:project) { create(:project, group: parent_group) }
+ let_it_be(:source) { project }
+ let_it_be(:member_type) { :project_member }
+ let_it_be(:mutation_name) { :project_member_bulk_update }
+ let_it_be(:source_id_key) { 'project_id' }
+ let_it_be(:response_member_field) { 'projectMembers' }
+
+ it_behaves_like 'members bulk update mutation'
+end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
index b5f2042c42a..d41628704a1 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
@@ -106,7 +106,7 @@ RSpec.describe 'Setting assignees of a merge request', :assume_throttled, featur
end
context 'when passing an empty list of assignees' do
- let(:db_query_limit) { 31 }
+ let(:db_query_limit) { 35 }
let(:input) { { assignee_usernames: [] } }
before do
diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
index bce57b47aab..d81744abe1b 100644
--- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
@@ -19,7 +19,11 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create, feature_categ
graphql_mutation_response(:create_annotation)
end
- specify { expect(described_class).to require_graphql_authorizations(:create_metrics_dashboard_annotation) }
+ before do
+ stub_feature_flags(remove_monitor_metrics: false)
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:admin_metrics_dashboard_annotation) }
context 'when annotation source is environment' do
let(:mutation) do
@@ -103,6 +107,15 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Create, feature_categ
it_behaves_like 'an invalid argument to the mutation', argument_name: :environment_id
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ end
end
end
diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
index f505dc25dc0..09977cd19d7 100644
--- a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/delete_spec.rb
@@ -17,7 +17,11 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete, feature_categ
graphql_mutation_response(:delete_annotation)
end
- specify { expect(described_class).to require_graphql_authorizations(:delete_metrics_dashboard_annotation) }
+ before do
+ stub_feature_flags(remove_monitor_metrics: false)
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:admin_metrics_dashboard_annotation) }
context 'when the user has permission to delete the annotation' do
before do
@@ -54,6 +58,15 @@ RSpec.describe Mutations::Metrics::Dashboard::Annotations::Delete, feature_categ
expect(mutation_response['errors']).to eq([service_response[:message]])
end
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ end
end
context 'when the user does not have permission to delete the annotation' do
diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
index a6253ba424b..e6feba059c4 100644
--- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
+++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb
@@ -104,7 +104,8 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
end
context 'as work item' do
- let(:noteable) { create(:work_item, :issue, project: project) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:noteable) { create(:work_item, :issue, project: project) }
context 'when using internal param' do
let(:variables_extra) { { internal: true } }
@@ -130,6 +131,20 @@ RSpec.describe 'Adding a Note', feature_category: :team_planning do
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
+
+ context 'when body contains quick actions' do
+ let_it_be(:noteable) { create(:work_item, :task, project: project) }
+
+ let(:variables_extra) { {} }
+
+ it_behaves_like 'work item supports labels widget updates via quick actions'
+ it_behaves_like 'work item does not support labels widget updates via quick actions'
+ it_behaves_like 'work item supports assignee widget updates via quick actions'
+ it_behaves_like 'work item does not support assignee widget updates via quick actions'
+ it_behaves_like 'work item supports start and due date widget updates via quick actions'
+ it_behaves_like 'work item does not support start and due date widget updates via quick actions'
+ it_behaves_like 'work item supports type change via quick actions'
+ end
end
end
diff --git a/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb b/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb
new file mode 100644
index 00000000000..c5dc6f390d9
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/projects/sync_fork_spec.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe "Sync project fork", feature_category: :source_code_management do
+ include GraphqlHelpers
+ include ProjectForksHelper
+ include ExclusiveLeaseHelpers
+
+ let_it_be(:source_project) { create(:project, :repository, :public) }
+ let_it_be(:current_user) { create(:user, maintainer_projects: [source_project]) }
+ let_it_be(:project, refind: true) { fork_project(source_project, current_user, { repository: true }) }
+ let_it_be(:target_branch) { project.default_branch }
+
+ let(:mutation) do
+ params = { project_path: project.full_path, target_branch: target_branch }
+
+ graphql_mutation(:project_sync_fork, params) do
+ <<-QL.strip_heredoc
+ details {
+ ahead
+ behind
+ isSyncing
+ hasConflicts
+ }
+ errors
+ QL
+ end
+ end
+
+ before do
+ source_project.change_head('feature')
+ end
+
+ context 'when synchronize_fork feature flag is disabled' do
+ before do
+ stub_feature_flags(synchronize_fork: false)
+ end
+
+ it 'does not call the sync service' do
+ expect(::Projects::Forks::SyncWorker).not_to receive(:perform_async)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_mutation_response(:project_sync_fork)).to eq(
+ {
+ 'details' => nil,
+ 'errors' => ['Feature flag is disabled']
+ })
+ end
+ end
+
+ context 'when the branch is protected', :use_clean_rails_redis_caching do
+ let_it_be(:protected_branch) do
+ create(:protected_branch, :no_one_can_push, project: project, name: target_branch)
+ end
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not call the sync service' do
+ expect(::Projects::Forks::SyncWorker).not_to receive(:perform_async)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+ end
+ end
+
+ context 'when the user does not have permission' do
+ let_it_be(:current_user) { create(:user) }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not call the sync service' do
+ expect(::Projects::Forks::SyncWorker).not_to receive(:perform_async)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+ end
+ end
+
+ context 'when the user has permission' do
+ context 'and the sync service executes successfully', :sidekiq_inline do
+ it 'calls the sync service' do
+ expect(::Projects::Forks::SyncWorker).to receive(:perform_async).and_call_original
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_mutation_response(:project_sync_fork)).to eq(
+ {
+ 'details' => { 'ahead' => 30, 'behind' => 0, "hasConflicts" => false, "isSyncing" => false },
+ 'errors' => []
+ })
+ end
+ end
+
+ context 'and the sync service fails to execute' do
+ let(:target_branch) { 'markdown' }
+
+ def expect_error_response(message)
+ expect(::Projects::Forks::SyncWorker).not_to receive(:perform_async)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_mutation_response(:project_sync_fork)['errors']).to eq([message])
+ end
+
+ context 'when fork details cannot be resolved' do
+ let_it_be(:project) { source_project }
+
+ it 'returns an error' do
+ expect_error_response('This branch of this project cannot be updated from the upstream')
+ end
+ end
+
+ context 'when the specified branch does not exist' do
+ let(:target_branch) { 'non-existent-branch' }
+
+ it 'returns an error' do
+ expect_error_response('Target branch does not exist')
+ end
+ end
+
+ context 'when the previous execution resulted in a conflict' do
+ it 'returns an error' do
+ expect_next_instance_of(::Projects::Forks::Details) do |instance|
+ expect(instance).to receive(:has_conflicts?).twice.and_return(true)
+ end
+
+ expect_error_response('The synchronization cannot happen due to the merge conflict')
+ expect(graphql_mutation_response(:project_sync_fork)['details']['hasConflicts']).to eq(true)
+ end
+ end
+
+ context 'when the request is rate limited' do
+ it 'returns an error' do
+ expect(Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(true)
+
+ expect_error_response('This service has been called too many times.')
+ end
+ end
+
+ context 'when another fork sync is in progress' do
+ it 'returns an error' do
+ expect_next_instance_of(Projects::Forks::Details) do |instance|
+ lease = instance_double(Gitlab::ExclusiveLease, try_obtain: false, exists?: true)
+ expect(instance).to receive(:exclusive_lease).twice.and_return(lease)
+ end
+
+ expect_error_response('Another fork sync is already in progress')
+ expect(graphql_mutation_response(:project_sync_fork)['details']['isSyncing']).to eq(true)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb b/spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb
index 418a0e47a36..311ff48a846 100644
--- a/spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/release_asset_links/create_spec.rb
@@ -32,7 +32,6 @@ RSpec.describe 'Creation of a new release asset link', feature_category: :releas
url
linkType
directAssetUrl
- external
}
errors
FIELDS
@@ -49,8 +48,7 @@ RSpec.describe 'Creation of a new release asset link', feature_category: :releas
name: mutation_arguments[:name],
url: mutation_arguments[:url],
linkType: mutation_arguments[:linkType],
- directAssetUrl: end_with(mutation_arguments[:directAssetPath]),
- external: true
+ directAssetUrl: end_with(mutation_arguments[:directAssetPath])
}.with_indifferent_access
expect(mutation_response[:link]).to include(expected_response)
diff --git a/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb b/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb
index b6d2c3f691d..cda1030c6d6 100644
--- a/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb
+++ b/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb
@@ -22,7 +22,6 @@ RSpec.describe 'Deletes a release asset link', feature_category: :release_orches
url
linkType
directAssetUrl
- external
}
errors
FIELDS
@@ -39,8 +38,7 @@ RSpec.describe 'Deletes a release asset link', feature_category: :release_orches
name: release_link.name,
url: release_link.url,
linkType: release_link.link_type.upcase,
- directAssetUrl: end_with(release_link.filepath),
- external: true
+ directAssetUrl: end_with(release_link.filepath)
}.with_indifferent_access
expect(mutation_response[:link]).to match(expected_response)
diff --git a/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb b/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb
index 61395cc4042..45028cba3ae 100644
--- a/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/release_asset_links/update_spec.rb
@@ -40,7 +40,6 @@ RSpec.describe 'Updating an existing release asset link', feature_category: :rel
url
linkType
directAssetUrl
- external
}
errors
FIELDS
@@ -57,8 +56,7 @@ RSpec.describe 'Updating an existing release asset link', feature_category: :rel
name: mutation_arguments[:name],
url: mutation_arguments[:url],
linkType: mutation_arguments[:linkType],
- directAssetUrl: end_with(mutation_arguments[:directAssetPath]),
- external: true
+ directAssetUrl: end_with(mutation_arguments[:directAssetPath])
}.with_indifferent_access
expect(mutation_response[:link]).to include(expected_response)
diff --git a/spec/requests/api/graphql/mutations/releases/create_spec.rb b/spec/requests/api/graphql/mutations/releases/create_spec.rb
index 295b8c0e97e..7cb421f17a3 100644
--- a/spec/requests/api/graphql/mutations/releases/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/releases/create_spec.rb
@@ -59,7 +59,6 @@ RSpec.describe 'Creation of a new release', feature_category: :release_orchestra
name
url
linkType
- external
directAssetUrl
}
}
@@ -135,7 +134,6 @@ RSpec.describe 'Creation of a new release', feature_category: :release_orchestra
name: asset_link[:name],
url: asset_link[:url],
linkType: asset_link[:linkType],
- external: true,
directAssetUrl: expected_direct_asset_url
}]
}
diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
index fa087e6773c..3b98ee3c2e9 100644
--- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
@@ -193,7 +193,6 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d
end
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:user) { current_user }
let(:property) { 'g_edit_by_snippet_ide' }
let(:namespace) { project.namespace }
@@ -203,8 +202,6 @@ RSpec.describe 'Updating a Snippet', feature_category: :source_code_management d
let(:context) do
[Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event_name).to_context]
end
-
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
end
end
end
diff --git a/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb
index 967ad75c906..65b8083c74f 100644
--- a/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb
@@ -11,7 +11,8 @@ RSpec.describe Mutations::UserPreferences::Update, feature_category: :user_profi
let(:input) do
{
- 'issuesSort' => sort_value
+ 'issuesSort' => sort_value,
+ 'visibilityPipelineIdType' => 'IID'
}
end
@@ -24,15 +25,20 @@ RSpec.describe Mutations::UserPreferences::Update, feature_category: :user_profi
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['userPreferences']['issuesSort']).to eq(sort_value)
+ expect(mutation_response['userPreferences']['visibilityPipelineIdType']).to eq('IID')
expect(current_user.user_preference.persisted?).to eq(true)
expect(current_user.user_preference.issues_sort).to eq(Types::IssueSortEnum.values[sort_value].value.to_s)
+ expect(current_user.user_preference.visibility_pipeline_id_type).to eq('iid')
end
end
context 'when user has existing preference' do
before do
- current_user.create_user_preference!(issues_sort: Types::IssueSortEnum.values['TITLE_DESC'].value)
+ current_user.create_user_preference!(
+ issues_sort: Types::IssueSortEnum.values['TITLE_DESC'].value,
+ visibility_pipeline_id_type: 'id'
+ )
end
it 'updates the existing value' do
@@ -42,8 +48,10 @@ RSpec.describe Mutations::UserPreferences::Update, feature_category: :user_profi
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['userPreferences']['issuesSort']).to eq(sort_value)
+ expect(mutation_response['userPreferences']['visibilityPipelineIdType']).to eq('IID')
expect(current_user.user_preference.issues_sort).to eq(Types::IssueSortEnum.values[sort_value].value.to_s)
+ expect(current_user.user_preference.visibility_pipeline_id_type).to eq('iid')
end
end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/convert_spec.rb b/spec/requests/api/graphql/mutations/work_items/convert_spec.rb
new file mode 100644
index 00000000000..97289597331
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/work_items/convert_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe "Converts a work item to a new type", feature_category: :team_planning do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
+ let_it_be(:new_type) { create(:work_item_type, :incident, :default) }
+ let_it_be(:work_item, refind: true) do
+ create(:work_item, :task, project: project, milestone: create(:milestone, project: project))
+ end
+
+ let(:work_item_type_id) { new_type.to_global_id.to_s }
+ let(:mutation) { graphql_mutation(:workItemConvert, input) }
+ let(:mutation_response) { graphql_mutation_response(:work_item_convert) }
+ let(:input) do
+ {
+ 'id' => work_item.to_global_id.to_s,
+ 'work_item_type_id' => work_item_type_id
+ }
+ end
+
+ context 'when user is not allowed to update a work item' do
+ let(:current_user) { create(:user) }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when user has permissions to convert the work item type' do
+ let(:current_user) { developer }
+
+ context 'when work item type does not exist' do
+ let(:work_item_type_id) { "gid://gitlab/WorkItems::Type/#{non_existing_record_id}" }
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors).to include(
+ a_hash_including('message' => "Work Item type with id #{non_existing_record_id} was not found")
+ )
+ end
+ end
+
+ it 'converts the work item', :aggregate_failures do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change { work_item.reload.work_item_type }.to(new_type)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(work_item.reload.work_item_type.base_type).to eq('incident')
+ expect(mutation_response['workItem']).to include('id' => work_item.to_global_id.to_s)
+ expect(work_item.reload.milestone).to be_nil
+ end
+
+ it_behaves_like 'has spam protection' do
+ let(:mutation_class) { ::Mutations::WorkItems::Convert }
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb
index 97bf060356a..6a6ad1b14fd 100644
--- a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe "Create a work item from a task in a work item's description", fe
}
end
- let(:mutation) { graphql_mutation(:workItemCreateFromTask, input) }
+ let(:mutation) { graphql_mutation(:workItemCreateFromTask, input, nil, ['productAnalyticsState']) }
let(:mutation_response) { graphql_mutation_response(:work_item_create_from_task) }
context 'the user is not allowed to update a work item' do
@@ -45,7 +45,6 @@ RSpec.describe "Create a work item from a task in a work item's description", fe
expect(response).to have_gitlab_http_status(:success)
expect(work_item.description).to eq("- [ ] #{created_work_item.to_reference}+")
- expect(created_work_item.issue_type).to eq('task')
expect(created_work_item.work_item_type.base_type).to eq('task')
expect(created_work_item.work_item_parent).to eq(work_item)
expect(created_work_item).to be_confidential
diff --git a/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
index 16f78b67b5c..fca3c84e534 100644
--- a/spec/requests/api/graphql/mutations/work_items/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb
@@ -5,52 +5,43 @@ require 'spec_helper'
RSpec.describe 'Create a work item', feature_category: :team_planning do
include GraphqlHelpers
- let_it_be(:project) { create(:project) }
- let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:developer) { create(:user).tap { |user| group.add_developer(user) } }
let(:input) do
{
'title' => 'new title',
'description' => 'new description',
'confidential' => true,
- 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_global_id.to_s
+ 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_gid.to_s
}
end
- let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path)) }
-
+ let(:fields) { nil }
let(:mutation_response) { graphql_mutation_response(:work_item_create) }
+ let(:current_user) { developer }
- context 'the user is not allowed to create a work item' do
- let(:current_user) { create(:user) }
-
- it_behaves_like 'a mutation that returns a top-level access error'
- end
-
- context 'when user has permissions to create a work item' do
- let(:current_user) { developer }
-
+ RSpec.shared_examples 'creates work item' do
it 'creates the work item' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to change(WorkItem, :count).by(1)
created_work_item = WorkItem.last
-
expect(response).to have_gitlab_http_status(:success)
- expect(created_work_item.issue_type).to eq('task')
expect(created_work_item).to be_confidential
expect(created_work_item.work_item_type.base_type).to eq('task')
expect(mutation_response['workItem']).to include(
input.except('workItemTypeId').merge(
- 'id' => created_work_item.to_global_id.to_s,
+ 'id' => created_work_item.to_gid.to_s,
'workItemType' => hash_including('name' => 'Task')
)
)
end
context 'when input is invalid' do
- let(:input) { { 'title' => '', 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_global_id.to_s } }
+ let(:input) { { 'title' => '', 'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_gid.to_s } }
it 'does not create and returns validation errors' do
expect do
@@ -90,16 +81,14 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
FIELDS
end
- let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) }
-
context 'when setting parent' do
- let_it_be(:parent) { create(:work_item, project: project) }
+ let_it_be(:parent) { create(:work_item, **container_params) }
let(:input) do
{
title: 'item1',
- workItemTypeId: WorkItems::Type.default_by_type(:task).to_global_id.to_s,
- hierarchyWidget: { 'parentId' => parent.to_global_id.to_s }
+ workItemTypeId: WorkItems::Type.default_by_type(:task).to_gid.to_s,
+ hierarchyWidget: { 'parentId' => parent.to_gid.to_s }
}
end
@@ -110,14 +99,14 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
expect(widgets_response).to include(
{
'children' => { 'edges' => [] },
- 'parent' => { 'id' => parent.to_global_id.to_s },
+ 'parent' => { 'id' => parent.to_gid.to_s },
'type' => 'HIERARCHY'
}
)
end
context 'when parent work item type is invalid' do
- let_it_be(:parent) { create(:work_item, :task, project: project) }
+ let_it_be(:parent) { create(:work_item, :task, **container_params) }
it 'returns error' do
post_graphql_mutation(mutation, current_user: current_user)
@@ -137,6 +126,40 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
expect(graphql_errors.first['message']).to include('No object found for `parentId')
end
end
+
+ context 'when adjacent is already in place' do
+ let_it_be(:adjacent) { create(:work_item, :task, **container_params) }
+
+ let(:work_item) { WorkItem.last }
+
+ let(:input) do
+ {
+ title: 'item1',
+ workItemTypeId: WorkItems::Type.default_by_type(:task).to_gid.to_s,
+ hierarchyWidget: { 'parentId' => parent.to_gid.to_s }
+ }
+ end
+
+ before(:all) do
+ create(:parent_link, work_item_parent: parent, work_item: adjacent, relative_position: 0)
+ end
+
+ it 'creates work item and sets the relative position to be AFTER adjacent' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change(WorkItem, :count).by(1)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(widgets_response).to include(
+ {
+ 'children' => { 'edges' => [] },
+ 'parent' => { 'id' => parent.to_gid.to_s },
+ 'type' => 'HIERARCHY'
+ }
+ )
+ expect(work_item.parent_link.relative_position).to be > adjacent.parent_link.relative_position
+ end
+ end
end
context 'when unsupported widget input is sent' do
@@ -144,7 +167,7 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
{
'title' => 'new title',
'description' => 'new description',
- 'workItemTypeId' => WorkItems::Type.default_by_type(:test_case).to_global_id.to_s,
+ 'workItemTypeId' => WorkItems::Type.default_by_type(:test_case).to_gid.to_s,
'hierarchyWidget' => {}
}
end
@@ -172,17 +195,15 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
FIELDS
end
- let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) }
-
context 'when setting milestone on work item creation' do
let_it_be(:project_milestone) { create(:milestone, project: project) }
- let_it_be(:group_milestone) { create(:milestone, project: project) }
+ let_it_be(:group_milestone) { create(:milestone, group: group) }
let(:input) do
{
title: 'some WI',
- workItemTypeId: WorkItems::Type.default_by_type(:task).to_global_id.to_s,
- milestoneWidget: { 'milestoneId' => milestone.to_global_id.to_s }
+ workItemTypeId: WorkItems::Type.default_by_type(:task).to_gid.to_s,
+ milestoneWidget: { 'milestoneId' => milestone.to_gid.to_s }
}
end
@@ -196,13 +217,18 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
expect(widgets_response).to include(
{
'type' => 'MILESTONE',
- 'milestone' => { 'id' => milestone.to_global_id.to_s }
+ 'milestone' => { 'id' => milestone.to_gid.to_s }
}
)
end
end
context 'when assigning a project milestone' do
+ before do
+ group_work_item = container_params[:namespace].present?
+ skip('cannot set a project level milestone to a group level work item') if group_work_item
+ end
+
it_behaves_like "work item's milestone is set" do
let(:milestone) { project_milestone }
end
@@ -216,4 +242,66 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do
end
end
end
+
+ context 'the user is not allowed to create a work item' do
+ let(:current_user) { create(:user) }
+ let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when user has permissions to create a work item' do
+ context 'when creating work items in a project' do
+ context 'with projectPath' do
+ let_it_be(:container_params) { { project: project } }
+ let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) }
+
+ it_behaves_like 'creates work item'
+ end
+
+ context 'with namespacePath' do
+ let_it_be(:container_params) { { project: project } }
+ let(:mutation) { graphql_mutation(:workItemCreate, input.merge('namespacePath' => project.full_path), fields) }
+
+ it_behaves_like 'creates work item'
+ end
+ end
+
+ context 'when creating work items in a group' do
+ let_it_be(:container_params) { { namespace: group } }
+ let(:mutation) { graphql_mutation(:workItemCreate, input.merge(namespacePath: group.full_path), fields) }
+
+ it_behaves_like 'creates work item'
+ end
+
+ context 'when both projectPath and namespacePath are passed' do
+ let_it_be(:container_params) { { project: project } }
+ let(:mutation) do
+ graphql_mutation(
+ :workItemCreate,
+ input.merge('projectPath' => project.full_path, 'namespacePath' => project.full_path),
+ fields
+ )
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: [
+ Mutations::WorkItems::Create::MUTUALLY_EXCLUSIVE_ARGUMENTS_ERROR
+ ]
+ end
+
+ context 'when neither of projectPath nor namespacePath are passed' do
+ let_it_be(:container_params) { { project: project } }
+ let(:mutation) do
+ graphql_mutation(
+ :workItemCreate,
+ input,
+ fields
+ )
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: [
+ Mutations::WorkItems::Create::MUTUALLY_EXCLUSIVE_ARGUMENTS_ERROR
+ ]
+ end
+ end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/export_spec.rb b/spec/requests/api/graphql/mutations/work_items/export_spec.rb
new file mode 100644
index 00000000000..d5d07ea65f8
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/work_items/export_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Export work items', feature_category: :team_planning do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:reporter) { create(:user).tap { |user| project.add_reporter(user) } }
+ let_it_be(:guest) { create(:user).tap { |user| project.add_guest(user) } }
+ let_it_be(:work_item) { create(:work_item, project: project) }
+
+ let(:input) { { 'projectPath' => project.full_path } }
+ let(:mutation) { graphql_mutation(:workItemExport, input) }
+ let(:mutation_response) { graphql_mutation_response(:work_item_export) }
+
+ context 'when user is not allowed to export work items' do
+ let(:current_user) { guest }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when import_export_work_items_csv feature flag is disabled' do
+ let(:current_user) { reporter }
+
+ before do
+ stub_feature_flags(import_export_work_items_csv: false)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: ['`import_export_work_items_csv` feature flag is disabled.']
+ end
+
+ context 'when user has permissions to export work items' do
+ let(:current_user) { reporter }
+ let(:input) do
+ super().merge(
+ 'selectedFields' => %w[TITLE DESCRIPTION AUTHOR TYPE AUTHOR_USERNAME CREATED_AT],
+ 'authorUsername' => 'admin',
+ 'iids' => [work_item.iid.to_s],
+ 'state' => 'opened',
+ 'types' => 'TASK',
+ 'search' => 'any',
+ 'in' => 'TITLE'
+ )
+ end
+
+ it 'schedules export job with given arguments', :aggregate_failures do
+ expected_arguments = {
+ selected_fields: ['title', 'description', 'author', 'type', 'author username', 'created_at'],
+ author_username: 'admin',
+ iids: [work_item.iid.to_s],
+ state: 'opened',
+ issue_types: ['task'],
+ search: 'any',
+ in: ['title']
+ }
+
+ expect(IssuableExportCsvWorker)
+ .to receive(:perform_async).with(:work_item, current_user.id, project.id, expected_arguments)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['message']).to eq(
+ 'Your CSV export request has succeeded. The result will be emailed to ' \
+ "#{reporter.notification_email_or_default}."
+ )
+ expect(mutation_response['errors']).to be_empty
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
index ddd294e8f82..ce1c2c01faa 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -7,20 +7,21 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:author) { create(:user).tap { |user| project.add_reporter(user) } }
let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
let_it_be(:reporter) { create(:user).tap { |user| project.add_reporter(user) } }
let_it_be(:guest) { create(:user).tap { |user| project.add_guest(user) } }
- let_it_be(:work_item, refind: true) { create(:work_item, project: project) }
+ let_it_be(:work_item, refind: true) { create(:work_item, project: project, author: author) }
let(:work_item_event) { 'CLOSE' }
let(:input) { { 'stateEvent' => work_item_event, 'title' => 'updated title' } }
let(:fields) do
<<~FIELDS
- workItem {
- state
- title
- }
- errors
+ workItem {
+ state
+ title
+ }
+ errors
FIELDS
end
@@ -81,10 +82,10 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
context 'when updating confidentiality' do
let(:fields) do
<<~FIELDS
- workItem {
- confidential
- }
- errors
+ workItem {
+ confidential
+ }
+ errors
FIELDS
end
@@ -126,18 +127,18 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
context 'with description widget input' do
let(:fields) do
<<~FIELDS
- workItem {
- title
- description
- state
- widgets {
- type
- ... on WorkItemWidgetDescription {
- description
+ workItem {
+ title
+ description
+ state
+ widgets {
+ type
+ ... on WorkItemWidgetDescription {
+ description
+ }
}
}
- }
- errors
+ errors
FIELDS
end
@@ -445,31 +446,84 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
let(:widgets_response) { mutation_response['workItem']['widgets'] }
let(:fields) do
<<~FIELDS
- workItem {
- description
- widgets {
- type
- ... on WorkItemWidgetHierarchy {
- parent {
- id
- }
- children {
- edges {
- node {
- id
+ workItem {
+ description
+ widgets {
+ type
+ ... on WorkItemWidgetHierarchy {
+ parent {
+ id
+ }
+ children {
+ edges {
+ node {
+ id
+ }
}
}
}
}
}
- }
- errors
+ errors
FIELDS
end
+ let_it_be(:valid_parent) { create(:work_item, project: project) }
+ let_it_be(:valid_child1) { create(:work_item, :task, project: project, created_at: 5.minutes.ago) }
+ let_it_be(:valid_child2) { create(:work_item, :task, project: project, created_at: 5.minutes.from_now) }
+ let(:input_base) { { parentId: valid_parent.to_gid.to_s } }
+ let(:child1_ref) { { adjacentWorkItemId: valid_child1.to_global_id.to_s } }
+ let(:child2_ref) { { adjacentWorkItemId: valid_child2.to_global_id.to_s } }
+ let(:relative_range) { [valid_child1, valid_child2].map(&:parent_link).map(&:relative_position) }
+
+ let(:invalid_relative_position_error) do
+ WorkItems::Widgets::HierarchyService::UpdateService::INVALID_RELATIVE_POSITION_ERROR
+ end
+
+ shared_examples 'updates work item parent and sets the relative position' do
+ it do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to change(work_item, :work_item_parent).from(nil).to(valid_parent)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(widgets_response).to include({ 'type' => 'HIERARCHY', 'children' => { 'edges' => [] },
+ 'parent' => { 'id' => valid_parent.to_global_id.to_s } })
+
+ expect(work_item.parent_link.relative_position).to be_between(*relative_range)
+ end
+ end
+
+ shared_examples 'sets the relative position and does not update work item parent' do
+ it do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to not_change(work_item, :work_item_parent)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(widgets_response).to include({ 'type' => 'HIERARCHY', 'children' => { 'edges' => [] },
+ 'parent' => { 'id' => valid_parent.to_global_id.to_s } })
+
+ expect(work_item.parent_link.relative_position).to be_between(*relative_range)
+ end
+ end
+
+ shared_examples 'returns "relative position is not valid" error message' do
+ it do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to not_change(work_item, :work_item_parent)
+
+ expect(mutation_response['workItem']).to be_nil
+ expect(mutation_response['errors']).to match_array([invalid_relative_position_error])
+ end
+ end
+
context 'when updating parent' do
let_it_be(:work_item, reload: true) { create(:work_item, :task, project: project) }
- let_it_be(:valid_parent) { create(:work_item, project: project) }
let_it_be(:invalid_parent) { create(:work_item, :task, project: project) }
context 'when parent work item type is invalid' do
@@ -492,20 +546,15 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
context 'when parent work item has a valid type' do
let(:input) { { 'hierarchyWidget' => { 'parentId' => valid_parent.to_global_id.to_s } } }
- it 'sets the parent for the work item' do
+ it 'updates work item parent' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
work_item.reload
end.to change(work_item, :work_item_parent).from(nil).to(valid_parent)
expect(response).to have_gitlab_http_status(:success)
- expect(widgets_response).to include(
- {
- 'children' => { 'edges' => [] },
- 'parent' => { 'id' => valid_parent.to_global_id.to_s },
- 'type' => 'HIERARCHY'
- }
- )
+ expect(widgets_response).to include({ 'type' => 'HIERARCHY', 'children' => { 'edges' => [] },
+ 'parent' => { 'id' => valid_parent.to_global_id.to_s } })
end
context 'when a parent is already present' do
@@ -522,6 +571,31 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
end.to change(work_item, :work_item_parent).from(existing_parent).to(valid_parent)
end
end
+
+ context 'when updating relative position' do
+ before(:all) do
+ create(:parent_link, work_item_parent: valid_parent, work_item: valid_child1)
+ create(:parent_link, work_item_parent: valid_parent, work_item: valid_child2)
+ end
+
+ context "when incomplete positioning arguments are given" do
+ let(:input) { { hierarchyWidget: input_base.merge(child1_ref) } }
+
+ it_behaves_like 'returns "relative position is not valid" error message'
+ end
+
+ context 'when moving after adjacent' do
+ let(:input) { { hierarchyWidget: input_base.merge(child1_ref).merge(relativePosition: 'AFTER') } }
+
+ it_behaves_like 'updates work item parent and sets the relative position'
+ end
+
+ context 'when moving before adjacent' do
+ let(:input) { { hierarchyWidget: input_base.merge(child2_ref).merge(relativePosition: 'BEFORE') } }
+
+ it_behaves_like 'updates work item parent and sets the relative position'
+ end
+ end
end
context 'when parentId is null' do
@@ -577,9 +651,37 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
end
end
+ context 'when reordering existing child' do
+ let_it_be(:work_item, reload: true) { create(:work_item, :task, project: project) }
+
+ context "when parent is already assigned" do
+ before(:all) do
+ create(:parent_link, work_item_parent: valid_parent, work_item: work_item)
+ create(:parent_link, work_item_parent: valid_parent, work_item: valid_child1)
+ create(:parent_link, work_item_parent: valid_parent, work_item: valid_child2)
+ end
+
+ context "when incomplete positioning arguments are given" do
+ let(:input) { { hierarchyWidget: child1_ref } }
+
+ it_behaves_like 'returns "relative position is not valid" error message'
+ end
+
+ context 'when moving after adjacent' do
+ let(:input) { { hierarchyWidget: child1_ref.merge(relativePosition: 'AFTER') } }
+
+ it_behaves_like 'sets the relative position and does not update work item parent'
+ end
+
+ context 'when moving before adjacent' do
+ let(:input) { { hierarchyWidget: child2_ref.merge(relativePosition: 'BEFORE') } }
+
+ it_behaves_like 'sets the relative position and does not update work item parent'
+ end
+ end
+ end
+
context 'when updating children' do
- let_it_be(:valid_child1) { create(:work_item, :task, project: project) }
- let_it_be(:valid_child2) { create(:work_item, :task, project: project) }
let_it_be(:invalid_child) { create(:work_item, project: project) }
let(:input) { { 'hierarchyWidget' => { 'childrenIds' => children_ids } } }
@@ -639,23 +741,29 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
context 'when updating assignees' do
let(:fields) do
<<~FIELDS
- workItem {
- widgets {
- type
- ... on WorkItemWidgetAssignees {
- assignees {
- nodes {
- id
- username
+ workItem {
+ title
+ workItemType { name }
+ widgets {
+ type
+ ... on WorkItemWidgetAssignees {
+ assignees {
+ nodes {
+ id
+ username
+ }
}
}
- }
- ... on WorkItemWidgetDescription {
- description
+ ... on WorkItemWidgetDescription {
+ description
+ }
+ ... on WorkItemWidgetStartAndDueDate {
+ startDate
+ dueDate
+ }
}
}
- }
- errors
+ errors
FIELDS
end
@@ -728,6 +836,79 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
)
end
end
+
+ context 'when changing work item type' do
+ let_it_be(:work_item) { create(:work_item, :task, project: project) }
+ let(:description) { "/type Issue" }
+
+ let(:input) { { 'descriptionWidget' => { 'description' => description } } }
+
+ context 'with multiple commands' do
+ let_it_be(:work_item) { create(:work_item, :task, project: project) }
+
+ let(:description) { "Updating work item\n/type Issue\n/due tomorrow\n/title Foo" }
+
+ it 'updates the work item type and other attributes' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to change { work_item.work_item_type.base_type }.from('task').to('issue')
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['workItemType']['name']).to eq('Issue')
+ expect(mutation_response['workItem']['title']).to eq('Foo')
+ expect(mutation_response['workItem']['widgets']).to include(
+ 'type' => 'START_AND_DUE_DATE',
+ 'dueDate' => Date.tomorrow.strftime('%Y-%m-%d'),
+ 'startDate' => nil
+ )
+ end
+ end
+
+ context 'when conversion is not permitted' do
+ let_it_be(:issue) { create(:work_item, project: project) }
+ let_it_be(:link) { create(:parent_link, work_item_parent: issue, work_item: work_item) }
+
+ let(:error_msg) { 'Work item type cannot be changed to Issue with Issue as parent type.' }
+
+ it 'does not update the work item type' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.not_to change { work_item.work_item_type.base_type }
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to include(error_msg)
+ end
+ end
+
+ context 'when new type does not support a widget' do
+ before do
+ work_item.update!(start_date: Date.current, due_date: Date.tomorrow)
+ WorkItems::Type.default_by_type(:issue).widget_definitions
+ .find_by_widget_type(:start_and_due_date).update!(disabled: true)
+ end
+
+ it 'updates the work item type and clear widget attributes' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ work_item.reload
+ end.to change { work_item.work_item_type.base_type }.from('task').to('issue')
+ .and change { work_item.start_date }.to(nil)
+ .and change { work_item.start_date }.to(nil)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['workItemType']['name']).to eq('Issue')
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'type' => 'START_AND_DUE_DATE',
+ 'startDate' => nil,
+ 'dueDate' => nil
+ }
+ )
+ end
+ end
+ end
end
context 'when the work item type does not support the assignees widget' do
@@ -766,17 +947,17 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
let(:fields) do
<<~FIELDS
- workItem {
- widgets {
- type
- ... on WorkItemWidgetMilestone {
- milestone {
- id
+ workItem {
+ widgets {
+ type
+ ... on WorkItemWidgetMilestone {
+ milestone {
+ id
+ }
}
}
}
- }
- errors
+ errors
FIELDS
end
@@ -843,18 +1024,427 @@ RSpec.describe 'Update a work item', feature_category: :team_planning do
end
end
+ context 'when updating notifications subscription' do
+ let_it_be(:current_user) { reporter }
+ let(:input) { { 'notificationsWidget' => { 'subscribed' => desired_state } } }
+
+ let(:fields) do
+ <<~FIELDS
+ workItem {
+ widgets {
+ type
+ ... on WorkItemWidgetNotifications {
+ subscribed
+ }
+ }
+ }
+ errors
+ FIELDS
+ end
+
+ subject(:update_work_item) { post_graphql_mutation(mutation, current_user: current_user) }
+
+ shared_examples 'subscription updated successfully' do
+ let_it_be(:subscription) do
+ create(
+ :subscription, project: project,
+ user: current_user,
+ subscribable: work_item,
+ subscribed: !desired_state
+ )
+ end
+
+ it "updates existing work item's subscription state" do
+ expect do
+ update_work_item
+ subscription.reload
+ end.to change(subscription, :subscribed).to(desired_state)
+ .and(change { work_item.reload.subscribed?(reporter, project) }.to(desired_state))
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'subscribed' => desired_state,
+ 'type' => 'NOTIFICATIONS'
+ }
+ )
+ end
+ end
+
+ shared_examples 'subscription update ignored' do
+ context 'when user is subscribed with a subscription record' do
+ let_it_be(:subscription) do
+ create(
+ :subscription, project: project,
+ user: current_user,
+ subscribable: work_item,
+ subscribed: !desired_state
+ )
+ end
+
+ it 'ignores the update request' do
+ expect do
+ update_work_item
+ subscription.reload
+ end.to not_change(subscription, :subscribed)
+ .and(not_change { work_item.subscribed?(current_user, project) })
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+
+ context 'when user is subscribed by being a participant' do
+ let_it_be(:current_user) { author }
+
+ it 'ignores the update request' do
+ expect do
+ update_work_item
+ end.to not_change(Subscription, :count)
+ .and(not_change { work_item.subscribed?(current_user, project) })
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+ end
+
+ context 'when work item update fails' do
+ let_it_be(:desired_state) { false }
+ let(:input) { { 'title' => nil, 'notificationsWidget' => { 'subscribed' => desired_state } } }
+
+ it_behaves_like 'subscription update ignored'
+ end
+
+ context 'when user cannot update work item' do
+ let_it_be(:desired_state) { false }
+
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(current_user, :update_subscription, work_item).and_return(false)
+ end
+
+ it_behaves_like 'subscription update ignored'
+ end
+
+ context 'when user can update work item' do
+ context 'when subscribing to notifications' do
+ let_it_be(:desired_state) { true }
+
+ it_behaves_like 'subscription updated successfully'
+ end
+
+ context 'when unsubscribing from notifications' do
+ let_it_be(:desired_state) { false }
+
+ it_behaves_like 'subscription updated successfully'
+
+ context 'when user is subscribed by being a participant' do
+ let_it_be(:current_user) { author }
+
+ it 'creates a subscription with desired state' do
+ expect { update_work_item }.to change(Subscription, :count).by(1)
+ .and(change { work_item.reload.subscribed?(author, project) }.to(desired_state))
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'subscribed' => desired_state,
+ 'type' => 'NOTIFICATIONS'
+ }
+ )
+ end
+ end
+ end
+ end
+ end
+
+ context 'when updating currentUserTodos' do
+ let_it_be(:current_user) { reporter }
+
+ let(:fields) do
+ <<~FIELDS
+ workItem {
+ widgets {
+ type
+ ... on WorkItemWidgetCurrentUserTodos {
+ currentUserTodos {
+ nodes {
+ id
+ state
+ }
+ }
+ }
+ }
+ }
+ errors
+ FIELDS
+ end
+
+ subject(:update_work_item) { post_graphql_mutation(mutation, current_user: current_user) }
+
+ context 'when adding a new todo' do
+ let(:input) { { 'currentUserTodosWidget' => { 'action' => 'ADD' } } }
+
+ context 'when user has access to the work item' do
+ it 'adds a new todo for the user on the work item' do
+ expect { update_work_item }.to change { current_user.todos.count }.by(1)
+
+ created_todo = current_user.todos.last
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'type' => 'CURRENT_USER_TODOS',
+ 'currentUserTodos' => {
+ 'nodes' => [
+ { 'id' => created_todo.to_global_id.to_s, 'state' => 'pending' }
+ ]
+ }
+ }
+ )
+ end
+ end
+
+ context 'when user has no access' do
+ let_it_be(:current_user) { create(:user) }
+
+ it 'does not create a new todo' do
+ expect { update_work_item }.to change { Todo.count }.by(0)
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+ end
+
+ context 'when marking all todos of the work item as done' do
+ let_it_be(:pending_todo1) do
+ create(:todo, target: work_item, target_type: 'WorkItem', user: current_user, state: :pending)
+ end
+
+ let_it_be(:pending_todo2) do
+ create(:todo, target: work_item, target_type: 'WorkItem', user: current_user, state: :pending)
+ end
+
+ let(:input) { { 'currentUserTodosWidget' => { 'action' => 'MARK_AS_DONE' } } }
+
+ context 'when user has access' do
+ it 'marks all todos of the user on the work item as done' do
+ expect { update_work_item }.to change { current_user.todos.done.count }.by(2)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'type' => 'CURRENT_USER_TODOS',
+ 'currentUserTodos' => {
+ 'nodes' => match_array([
+ { 'id' => pending_todo1.to_global_id.to_s, 'state' => 'done' },
+ { 'id' => pending_todo2.to_global_id.to_s, 'state' => 'done' }
+ ])
+ }
+ }
+ )
+ end
+ end
+
+ context 'when user has no access' do
+ let_it_be(:current_user) { create(:user) }
+
+ it 'does not mark todos as done' do
+ expect { update_work_item }.to change { Todo.done.count }.by(0)
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+ end
+
+ context 'when marking one todo of the work item as done' do
+ let_it_be(:pending_todo1) do
+ create(:todo, target: work_item, target_type: 'WorkItem', user: current_user, state: :pending)
+ end
+
+ let_it_be(:pending_todo2) do
+ create(:todo, target: work_item, target_type: 'WorkItem', user: current_user, state: :pending)
+ end
+
+ let(:input) do
+ { 'currentUserTodosWidget' => { 'action' => 'MARK_AS_DONE', todo_id: global_id_of(pending_todo1) } }
+ end
+
+ context 'when user has access' do
+ it 'marks the todo of the work item as done' do
+ expect { update_work_item }.to change { current_user.todos.done.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'type' => 'CURRENT_USER_TODOS',
+ 'currentUserTodos' => {
+ 'nodes' => match_array([
+ { 'id' => pending_todo1.to_global_id.to_s, 'state' => 'done' },
+ { 'id' => pending_todo2.to_global_id.to_s, 'state' => 'pending' }
+ ])
+ }
+ }
+ )
+ end
+ end
+
+ context 'when user has no access' do
+ let_it_be(:current_user) { create(:user) }
+
+ it 'does not mark the todo as done' do
+ expect { update_work_item }.to change { Todo.done.count }.by(0)
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+ end
+ end
+
+ context 'when updating awardEmoji' do
+ let_it_be(:current_user) { work_item.author }
+ let_it_be(:upvote) { create(:award_emoji, :upvote, awardable: work_item, user: current_user) }
+ let(:award_action) { 'ADD' }
+ let(:award_name) { 'star' }
+ let(:input) { { 'awardEmojiWidget' => { 'action' => award_action, 'name' => award_name } } }
+
+ let(:fields) do
+ <<~FIELDS
+ workItem {
+ widgets {
+ type
+ ... on WorkItemWidgetAwardEmoji {
+ upvotes
+ downvotes
+ awardEmoji {
+ nodes {
+ name
+ user { id }
+ }
+ }
+ }
+ }
+ }
+ errors
+ FIELDS
+ end
+
+ subject(:update_work_item) { post_graphql_mutation(mutation, current_user: current_user) }
+
+ context 'when user cannot award work item' do
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(current_user, :award_emoji, work_item).and_return(false)
+ end
+
+ it 'ignores the update request' do
+ expect do
+ update_work_item
+ end.to not_change(AwardEmoji, :count)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to be_empty
+ expect(graphql_errors).to be_blank
+ end
+ end
+
+ context 'when user can award work item' do
+ shared_examples 'request with error' do |message|
+ it 'ignores update and returns an error' do
+ expect do
+ update_work_item
+ end.not_to change(AwardEmoji, :count)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']).to be_nil
+ expect(mutation_response['errors'].first).to include(message)
+ end
+ end
+
+ shared_examples 'request that removes emoji' do
+ it "updates work item's award emoji" do
+ expect do
+ update_work_item
+ end.to change(AwardEmoji, :count).by(-1)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'upvotes' => 0,
+ 'downvotes' => 0,
+ 'awardEmoji' => { 'nodes' => [] },
+ 'type' => 'AWARD_EMOJI'
+ }
+ )
+ end
+ end
+
+ shared_examples 'request that adds emoji' do
+ it "updates work item's award emoji" do
+ expect do
+ update_work_item
+ end.to change(AwardEmoji, :count).by(1)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include(
+ {
+ 'upvotes' => 1,
+ 'downvotes' => 0,
+ 'awardEmoji' => { 'nodes' => [
+ { 'name' => 'thumbsup', 'user' => { 'id' => current_user.to_gid.to_s } },
+ { 'name' => award_name, 'user' => { 'id' => current_user.to_gid.to_s } }
+ ] },
+ 'type' => 'AWARD_EMOJI'
+ }
+ )
+ end
+ end
+
+ context 'when adding award emoji' do
+ it_behaves_like 'request that adds emoji'
+
+ context 'when the emoji name is not valid' do
+ let(:award_name) { 'xxqq' }
+
+ it_behaves_like 'request with error', 'Name is not a valid emoji name'
+ end
+ end
+
+ context 'when removing award emoji' do
+ let(:award_action) { 'REMOVE' }
+
+ context 'when emoji was awarded by current user' do
+ let(:award_name) { 'thumbsup' }
+
+ it_behaves_like 'request that removes emoji'
+ end
+
+ context 'when emoji was awarded by a different user' do
+ let(:award_name) { 'thumbsdown' }
+
+ before do
+ create(:award_emoji, :downvote, awardable: work_item)
+ end
+
+ it_behaves_like 'request with error',
+ 'User has not awarded emoji of type thumbsdown on the awardable'
+ end
+ end
+ end
+ end
+
context 'when unsupported widget input is sent' do
- let_it_be(:test_case) { create(:work_item_type, :default, :test_case) }
- let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) }
+ let_it_be(:work_item) { create(:work_item, :test_case, project: project) }
let(:input) do
{
- 'hierarchyWidget' => {}
+ 'assigneesWidget' => { 'assigneeIds' => [developer.to_gid.to_s] }
}
end
it_behaves_like 'a mutation that returns top-level errors',
- errors: ["Following widget keys are not supported by Test Case type: [:hierarchy_widget]"]
+ errors: ["Following widget keys are not supported by Test Case type: [:assignees_widget]"]
end
end
end
diff --git a/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb
index 999c685ac6a..717de983871 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_task_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'Update a work item task', feature_category: :team_planning do
let(:task_params) { { 'title' => 'UPDATED' } }
let(:task_input) { { 'id' => task.to_global_id.to_s }.merge(task_params) }
let(:input) { { 'id' => work_item.to_global_id.to_s, 'taskData' => task_input } }
- let(:mutation) { graphql_mutation(:workItemUpdateTask, input) }
+ let(:mutation) { graphql_mutation(:workItemUpdateTask, input, nil, ['productAnalyticsState']) }
let(:mutation_response) { graphql_mutation_response(:work_item_update_task) }
context 'the user is not allowed to read a work item' do
diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb
index 4e12da3e3ab..83edacaf831 100644
--- a/spec/requests/api/graphql/namespace/projects_spec.rb
+++ b/spec/requests/api/graphql/namespace/projects_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'getting projects', feature_category: :projects do
projects(includeSubgroups: #{include_subgroups}) {
edges {
node {
- #{all_graphql_fields_for('Project', max_depth: 1)}
+ #{all_graphql_fields_for('Project', max_depth: 1, excluded: ['productAnalyticsState'])}
}
}
}
diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb
index 82fcc5254ad..7610a4aaac1 100644
--- a/spec/requests/api/graphql/packages/package_spec.rb
+++ b/spec/requests/api/graphql/packages/package_spec.rb
@@ -270,6 +270,31 @@ RSpec.describe 'package details', feature_category: :package_registry do
it 'returns composer_config_repository_url correctly' do
expect(graphql_data_at(:package, :composer_config_repository_url)).to eq("localhost/#{group.id}")
end
+
+ context 'with access to package registry for everyone' do
+ before do
+ project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
+ subject
+ end
+
+ it 'returns pypi_url correctly' do
+ expect(graphql_data_at(:package, :pypi_url)).to eq("http://__token__:<your_personal_token>@localhost/api/v4/projects/#{project.id}/packages/pypi/simple")
+ end
+ end
+
+ context 'when project is public' do
+ let_it_be(:public_project) { create(:project, :public, group: group) }
+ let_it_be(:composer_package) { create(:composer_package, project: public_project) }
+ let(:package_global_id) { global_id_of(composer_package) }
+
+ before do
+ subject
+ end
+
+ it 'returns pypi_url correctly' do
+ expect(graphql_data_at(:package, :pypi_url)).to eq("http://localhost/api/v4/projects/#{public_project.id}/packages/pypi/simple")
+ end
+ end
end
context 'web_path' do
diff --git a/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb
index b430fdeb18f..3417f9529bd 100644
--- a/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alert/metrics_dashboard_url_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :projects do
+RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :incident_management do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
@@ -29,6 +29,7 @@ RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :pr
let(:first_alert) { alerts.first }
before do
+ stub_feature_flags(remove_monitor_metrics: false)
project.add_developer(current_user)
end
@@ -44,6 +45,17 @@ RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :pr
expect(first_alert).to include('metricsDashboardUrl' => dashboard_url_for_alert)
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'returns nil' do
+ post_graphql(graphql_query, current_user: current_user)
+ expect(first_alert['metricsDashboardUrl']).to be_nil
+ end
+ end
end
context 'with gitlab-managed prometheus payload' do
@@ -58,5 +70,16 @@ RSpec.describe 'getting Alert Management Alert Assignees', feature_category: :pr
expect(first_alert).to include('metricsDashboardUrl' => dashboard_url_for_alert)
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'returns nil' do
+ post_graphql(graphql_query, current_user: current_user)
+ expect(first_alert['metricsDashboardUrl']).to be_nil
+ end
+ end
end
end
diff --git a/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb
index 16dd0dfcfcb..c1ac0367853 100644
--- a/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb
@@ -51,7 +51,7 @@ RSpec.describe 'getting Alert Management Alert Notes', feature_category: :team_p
expect(first_notes_result.first).to include(
'id' => first_system_note.to_global_id.to_s,
- 'systemNoteIconName' => 'git-merge',
+ 'systemNoteIconName' => 'merge',
'body' => first_system_note.note
)
end
diff --git a/spec/requests/api/graphql/project/base_service_spec.rb b/spec/requests/api/graphql/project/base_service_spec.rb
index 7b1b95eaf58..b27cddea07b 100644
--- a/spec/requests/api/graphql/project/base_service_spec.rb
+++ b/spec/requests/api/graphql/project/base_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'query Jira service', feature_category: :authentication_and_authorization do
+RSpec.describe 'query Jira service', feature_category: :system_access do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb b/spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb
new file mode 100644
index 00000000000..dd76f6425fe
--- /dev/null
+++ b/spec/requests/api/graphql/project/ci_access_authorized_agents_spec.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project.ci_access_authorized_agents', feature_category: :deployment_management do
+ include GraphqlHelpers
+
+ let_it_be(:organization) { create(:group) }
+ let_it_be(:agent_management_project) { create(:project, :private, group: organization) }
+ let_it_be(:agent) { create(:cluster_agent, project: agent_management_project) }
+
+ let_it_be(:deployment_project) { create(:project, :private, group: organization) }
+ let_it_be(:deployment_developer) { create(:user).tap { |u| deployment_project.add_developer(u) } }
+ let_it_be(:deployment_reporter) { create(:user).tap { |u| deployment_project.add_reporter(u) } }
+
+ let(:user) { deployment_developer }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{deployment_project.full_path}") {
+ ciAccessAuthorizedAgents {
+ nodes {
+ agent {
+ id
+ name
+ project {
+ name
+ }
+ }
+ config
+ }
+ }
+ }
+ }
+ )
+ end
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ context 'with project authorization' do
+ let!(:ci_access) { create(:agent_ci_access_project_authorization, agent: agent, project: deployment_project) }
+
+ it 'returns the authorized agent' do
+ authorized_agents = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes')
+
+ expect(authorized_agents.count).to eq(1)
+
+ authorized_agent = authorized_agents.first
+
+ expect(authorized_agent['agent']['id']).to eq(agent.to_global_id.to_s)
+ expect(authorized_agent['agent']['name']).to eq(agent.name)
+ expect(authorized_agent['config']).to eq({ "default_namespace" => "production" })
+ expect(authorized_agent['agent']['project']).to be_nil # User is not authorized to read other resources.
+ end
+
+ context 'when user is developer in the agent management project' do
+ before do
+ agent_management_project.add_developer(deployment_developer)
+ end
+
+ it 'returns the project information as well' do
+ authorized_agent = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes').first
+
+ expect(authorized_agent['agent']['project']['name']).to eq(agent_management_project.name)
+ end
+ end
+
+ context 'when user is reporter' do
+ let(:user) { deployment_reporter }
+
+ it 'returns nothing' do
+ expect(subject['data']['project']['ciAccessAuthorizedAgents']).to be_nil
+ end
+ end
+ end
+
+ context 'with group authorization' do
+ let!(:ci_access) { create(:agent_ci_access_group_authorization, agent: agent, group: organization) }
+
+ it 'returns the authorized agent' do
+ authorized_agents = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes')
+
+ expect(authorized_agents.count).to eq(1)
+
+ authorized_agent = authorized_agents.first
+
+ expect(authorized_agent['agent']['id']).to eq(agent.to_global_id.to_s)
+ expect(authorized_agent['agent']['name']).to eq(agent.name)
+ expect(authorized_agent['config']).to eq({ "default_namespace" => "production" })
+ expect(authorized_agent['agent']['project']).to be_nil # User is not authorized to read other resources.
+ end
+
+ context 'when user is developer in the agent management project' do
+ before do
+ agent_management_project.add_developer(deployment_developer)
+ end
+
+ it 'returns the project information as well' do
+ authorized_agent = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes').first
+
+ expect(authorized_agent['agent']['project']['name']).to eq(agent_management_project.name)
+ end
+ end
+
+ context 'when user is reporter' do
+ let(:user) { deployment_reporter }
+
+ it 'returns nothing' do
+ expect(subject['data']['project']['ciAccessAuthorizedAgents']).to be_nil
+ end
+ end
+ end
+
+ context 'when deployment project is not authorized to ci_access to the agent' do
+ it 'returns empty' do
+ authorized_agents = subject.dig('data', 'project', 'ciAccessAuthorizedAgents', 'nodes')
+
+ expect(authorized_agents).to be_empty
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/cluster_agents_spec.rb b/spec/requests/api/graphql/project/cluster_agents_spec.rb
index 0881eb9cdc3..181f21001ea 100644
--- a/spec/requests/api/graphql/project/cluster_agents_spec.rb
+++ b/spec/requests/api/graphql/project/cluster_agents_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project.cluster_agents', feature_category: :kubernetes_management do
+RSpec.describe 'Project.cluster_agents', feature_category: :deployment_management do
include GraphqlHelpers
let_it_be(:project) { create(:project, :public) }
@@ -53,10 +53,11 @@ RSpec.describe 'Project.cluster_agents', feature_category: :kubernetes_managemen
let_it_be(:token_1) { create(:cluster_agent_token, agent: agents.second) }
let_it_be(:token_2) { create(:cluster_agent_token, agent: agents.second, last_used_at: 3.days.ago) }
let_it_be(:token_3) { create(:cluster_agent_token, agent: agents.second, last_used_at: 2.days.ago) }
+ let_it_be(:revoked_token) { create(:cluster_agent_token, :revoked, agent: agents.second) }
let(:cluster_agents_fields) { [:id, query_nodes(:tokens, of: 'ClusterAgentToken')] }
- it 'can select tokens in last_used_at order' do
+ it 'can select active tokens in last_used_at order' do
post_graphql(query, current_user: current_user)
tokens = graphql_data_at(:project, :cluster_agents, :nodes, :tokens, :nodes)
diff --git a/spec/requests/api/graphql/project/commit_references_spec.rb b/spec/requests/api/graphql/project/commit_references_spec.rb
new file mode 100644
index 00000000000..4b545adee12
--- /dev/null
+++ b/spec/requests/api/graphql/project/commit_references_spec.rb
@@ -0,0 +1,240 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project(fullPath).commitReferences(commitSha)', feature_category: :source_code_management do
+ include GraphqlHelpers
+ include Presentable
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:repository) { project.repository.raw }
+ let_it_be(:current_user) { project.first_owner }
+ let_it_be(:branches_names) { %w[master not-merged-branch v1.1.0] }
+ let_it_be(:tag_name) { 'v1.0.0' }
+ let_it_be(:commit_sha) { repository.commit.id }
+
+ let(:post_query) { post_graphql(query, current_user: current_user) }
+ let(:data) { graphql_data.dig(*path) }
+ let(:base_args) { {} }
+ let(:args) { base_args }
+
+ shared_context 'with the limit argument' do
+ context 'with limit of 2' do
+ let(:args) { { limit: 2 } }
+
+ it 'returns the right amount of refs' do
+ post_query
+ expect(data.count).to be <= 2
+ end
+ end
+
+ context 'with limit of -2' do
+ let(:args) { { limit: -2 } }
+
+ it 'casts an argument error "limit must be greater then 0"' do
+ post_query
+ expect(graphql_errors).to include(custom_graphql_error(path - ['names'],
+ 'limit must be within 1..1000'))
+ end
+ end
+
+ context 'with limit of 1001' do
+ let(:args) { { limit: 1001 } }
+
+ it 'casts an argument error "limit must be greater then 0"' do
+ post_query
+ expect(graphql_errors).to include(custom_graphql_error(path - ['names'],
+ 'limit must be within 1..1000'))
+ end
+ end
+ end
+
+ describe 'the path commitReferences should return nil' do
+ let(:path) { %w[project commitReferences] }
+
+ let(:query) do
+ graphql_query_for(:project, { fullPath: project.full_path },
+ query_graphql_field(
+ :commitReferences,
+ { commitSha: commit_sha },
+ query_graphql_field(:tippingTags, :names)
+ )
+ )
+ end
+
+ context 'when commit does not exist' do
+ let(:commit_sha) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff4' }
+
+ it 'commitReferences returns nil' do
+ post_query
+ expect(data).to eq(nil)
+ end
+ end
+
+ context 'when sha length is incorrect' do
+ let(:commit_sha) { 'foo' }
+
+ it 'commitReferences returns nil' do
+ post_query
+ expect(data).to eq(nil)
+ end
+ end
+
+ context 'when user is not authorized' do
+ let(:commit_sha) { repository.commit.id }
+ let(:current_user) { create(:user) }
+
+ it 'commitReferences returns nil' do
+ post_query
+ expect(data).to eq(nil)
+ end
+ end
+ end
+
+ context 'with containing refs' do
+ let(:base_args) { { excludeTipped: false } }
+ let(:excluded_tipped_args) do
+ hash = base_args.dup
+ hash[:excludeTipped] = true
+ hash
+ end
+
+ context 'with path Query.project(fullPath).commitReferences(commitSha).containingTags' do
+ let_it_be(:commit_sha) { repository.find_tag(tag_name).target_commit.sha }
+ let_it_be(:path) { %w[project commitReferences containingTags names] }
+ let(:query) do
+ graphql_query_for(
+ :project,
+ { fullPath: project.full_path },
+ query_graphql_field(
+ :commitReferences,
+ { commitSha: commit_sha },
+ query_graphql_field(:containingTags, args, :names)
+ )
+ )
+ end
+
+ context 'without excludeTipped argument' do
+ it 'returns tags names containing the commit' do
+ post_query
+ expect(data).to eq(%w[v1.0.0 v1.1.0 v1.1.1])
+ end
+ end
+
+ context 'with excludeTipped argument' do
+ let_it_be(:ref_prefix) { Gitlab::Git::TAG_REF_PREFIX }
+
+ let(:args) { excluded_tipped_args }
+
+ it 'returns tags names containing the commit without the tipped tags' do
+ excluded_refs = project.repository
+ .refs_by_oid(oid: commit_sha, ref_patterns: [ref_prefix])
+ .map { |n| n.delete_prefix(ref_prefix) }
+
+ post_query
+ expect(data).to eq(%w[v1.0.0 v1.1.0 v1.1.1] - excluded_refs)
+ end
+ end
+
+ include_context 'with the limit argument'
+ end
+
+ context 'with path Query.project(fullPath).commitReferences(commitSha).containingBranches' do
+ let_it_be(:ref_prefix) { Gitlab::Git::BRANCH_REF_PREFIX }
+ let_it_be(:path) { %w[project commitReferences containingBranches names] }
+
+ let(:query) do
+ graphql_query_for(
+ :project,
+ { fullPath: project.full_path },
+ query_graphql_field(
+ :commitReferences,
+ { commitSha: commit_sha },
+ query_graphql_field(:containingBranches, args, :names)
+ )
+ )
+ end
+
+ context 'without excludeTipped argument' do
+ it 'returns branch names containing the commit' do
+ refs = project.repository.branch_names_contains(commit_sha)
+
+ post_query
+
+ expect(data).to eq(refs)
+ end
+ end
+
+ context 'with excludeTipped argument' do
+ let(:args) { excluded_tipped_args }
+
+ it 'returns branch names containing the commit without the tipped branch' do
+ refs = project.repository.branch_names_contains(commit_sha)
+
+ excluded_refs = project.repository
+ .refs_by_oid(oid: commit_sha, ref_patterns: [ref_prefix])
+ .map { |n| n.delete_prefix(ref_prefix) }
+
+ post_query
+
+ expect(data).to eq(refs - excluded_refs)
+ end
+ end
+
+ include_context 'with the limit argument'
+ end
+ end
+
+ context 'with tipping refs' do
+ context 'with path Query.project(fullPath).commitReferences(commitSha).tippingTags' do
+ let(:commit_sha) { repository.find_tag(tag_name).dereferenced_target.sha }
+ let(:path) { %w[project commitReferences tippingTags names] }
+
+ let(:query) do
+ graphql_query_for(
+ :project,
+ { fullPath: project.full_path },
+ query_graphql_field(
+ :commitReferences,
+ { commitSha: commit_sha },
+ query_graphql_field(:tippingTags, args, :names)
+ )
+ )
+ end
+
+ context 'with authorized user' do
+ it 'returns tags names tipping the commit' do
+ post_query
+
+ expect(data).to eq([tag_name])
+ end
+ end
+
+ include_context 'with the limit argument'
+ end
+
+ context 'with path Query.project(fullPath).commitReferences(commitSha).tippingBranches' do
+ let(:path) { %w[project commitReferences tippingBranches names] }
+
+ let(:query) do
+ graphql_query_for(
+ :project,
+ { fullPath: project.full_path },
+ query_graphql_field(
+ :commitReferences,
+ { commitSha: commit_sha },
+ query_graphql_field(:tippingBranches, args, :names)
+ )
+ )
+ end
+
+ it 'returns branches names tipping the commit' do
+ post_query
+
+ expect(data).to eq(branches_names)
+ end
+
+ include_context 'with the limit argument'
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb
index 7ccf8a6f5bf..9a40a972256 100644
--- a/spec/requests/api/graphql/project/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/project/container_repositories_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'getting container repositories in a project', feature_category:
let_it_be(:container_repositories) { [container_repository, container_repositories_delete_scheduled, container_repositories_delete_failed].flatten }
let_it_be(:container_expiration_policy) { project.container_expiration_policy }
- let(:excluded_fields) { %w[pipeline jobs] }
+ let(:excluded_fields) { %w[pipeline jobs productAnalyticsState] }
let(:container_repositories_fields) do
<<~GQL
edges {
@@ -155,7 +155,7 @@ RSpec.describe 'getting container repositories in a project', feature_category:
it_behaves_like 'handling graphql network errors with the container registry'
it_behaves_like 'not hitting graphql network errors with the container registry' do
- let(:excluded_fields) { %w[pipeline jobs tags tagsCount] }
+ let(:excluded_fields) { %w[pipeline jobs tags tagsCount productAnalyticsState] }
end
it 'returns the total count of container repositories' do
diff --git a/spec/requests/api/graphql/project/data_transfer_spec.rb b/spec/requests/api/graphql/project/data_transfer_spec.rb
new file mode 100644
index 00000000000..aafa8d65eb9
--- /dev/null
+++ b/spec/requests/api/graphql/project/data_transfer_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'project data transfers', feature_category: :source_code_management do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ let(:fields) do
+ <<~QUERY
+ #{all_graphql_fields_for('ProjectDataTransfer'.classify)}
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { fullPath: project.full_path },
+ query_graphql_field('DataTransfer', params, fields)
+ )
+ end
+
+ let(:from) { Date.new(2022, 1, 1) }
+ let(:to) { Date.new(2023, 1, 1) }
+ let(:params) { { from: from, to: to } }
+ let(:egress_data) do
+ graphql_data.dig('project', 'dataTransfer', 'egressNodes', 'nodes')
+ end
+
+ before do
+ create(:project_data_transfer, project: project, date: '2022-01-01', repository_egress: 1)
+ create(:project_data_transfer, project: project, date: '2022-02-01', repository_egress: 2)
+ end
+
+ subject { post_graphql(query, current_user: current_user) }
+
+ context 'with anonymous access' do
+ let_it_be(:current_user) { nil }
+
+ before do
+ subject
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns no data' do
+ expect(graphql_data_at(:project, :data_transfer)).to be_nil
+ expect(graphql_errors).to be_nil
+ end
+ end
+
+ context 'with authorized user but without enough permissions' do
+ before do
+ project.add_developer(current_user)
+ subject
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns empty results' do
+ expect(graphql_data_at(:project, :data_transfer)).to be_nil
+ expect(graphql_errors).to be_nil
+ end
+ end
+
+ context 'when user has enough permissions' do
+ before do
+ project.add_owner(current_user)
+ end
+
+ context 'when data_transfer_monitoring_mock_data is NOT enabled' do
+ before do
+ stub_feature_flags(data_transfer_monitoring_mock_data: false)
+ subject
+ end
+
+ it 'returns real results' do
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(egress_data.count).to eq(2)
+
+ expect(egress_data.first.keys).to match_array(
+ %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
+ )
+
+ expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 2])
+ end
+
+ it_behaves_like 'a working graphql query'
+ end
+
+ context 'when data_transfer_monitoring_mock_data is enabled' do
+ before do
+ stub_feature_flags(data_transfer_monitoring_mock_data: true)
+ subject
+ end
+
+ it 'returns mock results' do
+ expect(response).to have_gitlab_http_status(:ok)
+
+ expect(egress_data.count).to eq(12)
+ expect(egress_data.first.keys).to match_array(
+ %w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
+ )
+ end
+
+ it_behaves_like 'a working graphql query'
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/environments_spec.rb b/spec/requests/api/graphql/project/environments_spec.rb
index 618f591affa..bb1763ee228 100644
--- a/spec/requests/api/graphql/project/environments_spec.rb
+++ b/spec/requests/api/graphql/project/environments_spec.rb
@@ -102,7 +102,7 @@ RSpec.describe 'Project Environments query', feature_category: :continuous_deliv
end
describe 'last deployments of environments' do
- ::Deployment.statuses.each do |status, _|
+ ::Deployment.statuses.each do |status, _| # rubocop:disable RSpec/UselessDynamicDefinition
let_it_be(:"production_#{status}_deployment") do
create(:deployment, status.to_sym, environment: production, project: project)
end
diff --git a/spec/requests/api/graphql/project/flow_metrics_spec.rb b/spec/requests/api/graphql/project/flow_metrics_spec.rb
new file mode 100644
index 00000000000..3b5758b3a2e
--- /dev/null
+++ b/spec/requests/api/graphql/project/flow_metrics_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting project flow metrics', feature_category: :value_stream_management do
+ include GraphqlHelpers
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project1) { create(:project, :repository, group: group) }
+ # This is done so we can use the same count expectations in the shared examples and
+ # reuse the shared example for the group-level test.
+ let_it_be(:project2) { project1 }
+ let_it_be(:production_environment1) { create(:environment, :production, project: project1) }
+ let_it_be(:production_environment2) { production_environment1 }
+ let_it_be(:current_user) { create(:user, maintainer_projects: [project1]) }
+
+ let(:full_path) { project1.full_path }
+ let(:context) { :project }
+
+ it_behaves_like 'value stream analytics flow metrics issueCount examples'
+
+ it_behaves_like 'value stream analytics flow metrics deploymentCount examples'
+end
diff --git a/spec/requests/api/graphql/project/fork_details_spec.rb b/spec/requests/api/graphql/project/fork_details_spec.rb
index efd48b00833..91a04dc7c50 100644
--- a/spec/requests/api/graphql/project/fork_details_spec.rb
+++ b/spec/requests/api/graphql/project/fork_details_spec.rb
@@ -10,12 +10,13 @@ RSpec.describe 'getting project fork details', feature_category: :source_code_ma
let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }
let_it_be(:forked_project) { fork_project(project, current_user, repository: true) }
+ let(:ref) { 'feature' }
let(:queried_project) { forked_project }
let(:query) do
graphql_query_for(:project,
{ full_path: queried_project.full_path }, <<~QUERY
- forkDetails(ref: "feature"){
+ forkDetails(ref: "#{ref}"){
ahead
behind
}
@@ -23,12 +24,23 @@ RSpec.describe 'getting project fork details', feature_category: :source_code_ma
)
end
- it 'returns fork details' do
- post_graphql(query, current_user: current_user)
+ context 'when a ref is specified' do
+ using RSpec::Parameterized::TableSyntax
- expect(graphql_data['project']['forkDetails']).to eq(
- { 'ahead' => 1, 'behind' => 29 }
- )
+ where(:ref, :counts) do
+ 'feature' | { 'ahead' => 1, 'behind' => 29 }
+ 'v1.1.1' | { 'ahead' => 5, 'behind' => 0 }
+ '7b5160f9bb23a3d58a0accdbe89da13b96b1ece9' | { 'ahead' => 9, 'behind' => 0 }
+ 'non-existent-branch' | { 'ahead' => nil, 'behind' => nil }
+ end
+
+ with_them do
+ it 'returns fork details' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']['forkDetails']).to eq(counts)
+ end
+ end
end
context 'when a project is not a fork' do
@@ -41,6 +53,16 @@ RSpec.describe 'getting project fork details', feature_category: :source_code_ma
end
end
+ context 'when project source is not visible' do
+ it 'does not return fork details' do
+ project.team.truncate
+
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']['forkDetails']).to be_nil
+ end
+ end
+
context 'when a user cannot read the code' do
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
index 76e5d687fd1..80c7258c05d 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -480,4 +480,31 @@ RSpec.describe 'getting merge request information nested in a project', feature_
merge_request.assignees << user
end
end
+
+ context 'when selecting `awardEmoji`' do
+ let_it_be(:award_emoji) { create(:award_emoji, awardable: merge_request, user: current_user) }
+
+ let(:mr_fields) do
+ <<~QUERY
+ awardEmoji {
+ nodes {
+ user {
+ username
+ }
+ name
+ }
+ }
+ QUERY
+ end
+
+ it 'includes award emojis' do
+ post_graphql(query, current_user: current_user)
+
+ response = merge_request_graphql_data['awardEmoji']['nodes']
+
+ expect(response.length).to eq(1)
+ expect(response.first['user']['username']).to eq(current_user.username)
+ expect(response.first['name']).to eq(award_emoji.name)
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index 8407faa967e..e3c4396e7d8 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -226,6 +226,28 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat
it_behaves_like 'when searching with parameters'
end
+ context 'when searching by approved' do
+ let(:approved_mr) { create(:merge_request, target_project: project, source_project: project) }
+
+ before do
+ create(:approval, merge_request: approved_mr)
+ end
+
+ context 'when true' do
+ let(:search_params) { { approved: true } }
+ let(:mrs) { [approved_mr] }
+
+ it_behaves_like 'when searching with parameters'
+ end
+
+ context 'when false' do
+ let(:search_params) { { approved: false } }
+ let(:mrs) { all_merge_requests }
+
+ it_behaves_like 'when searching with parameters'
+ end
+ end
+
context 'when requesting `approved_by`' do
let(:search_params) { { iids: [merge_request_a.iid.to_s, merge_request_b.iid.to_s] } }
let(:extra_iid_for_second_query) { merge_request_c.iid.to_s }
@@ -331,7 +353,7 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat
end
context 'when award emoji votes' do
- let(:requested_fields) { [:upvotes, :downvotes] }
+ let(:requested_fields) { 'upvotes downvotes awardEmoji { nodes { name } }' }
before do
create_list(:award_emoji, 2, name: 'thumbsup', awardable: merge_request_a)
@@ -588,8 +610,9 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat
end
let(:query) do
+ # Adding a no-op `not` filter to mimic the same query as the frontend does
graphql_query_for(:project, { full_path: project.full_path }, <<~QUERY)
- mergeRequests(mergedAfter: "2020-01-01", mergedBefore: "2020-01-05", first: 0) {
+ mergeRequests(mergedAfter: "2020-01-01", mergedBefore: "2020-01-05", first: 0, not: { labels: null }) {
totalTimeToMerge
count
}
diff --git a/spec/requests/api/graphql/project/milestones_spec.rb b/spec/requests/api/graphql/project/milestones_spec.rb
index 3b31da77a75..7a79bf2184a 100644
--- a/spec/requests/api/graphql/project/milestones_spec.rb
+++ b/spec/requests/api/graphql/project/milestones_spec.rb
@@ -137,18 +137,6 @@ RSpec.describe 'getting milestone listings nested in a project', feature_categor
it_behaves_like 'searching with parameters'
end
- context 'searching by custom range' do
- let(:expected) { [no_end, fully_future] }
- let(:search_params) do
- {
- start_date: (today + 6.days).iso8601,
- end_date: (today + 7.days).iso8601
- }
- end
-
- it_behaves_like 'searching with parameters'
- end
-
context 'using timeframe argument' do
let(:expected) { [no_end, fully_future] }
let(:search_params) do
@@ -188,23 +176,6 @@ RSpec.describe 'getting milestone listings nested in a project', feature_categor
end
end
- it 'is invalid to provide timeframe and start_date/end_date' do
- query = <<~GQL
- query($path: ID!, $tstart: Date!, $tend: Date!, $start: Time!, $end: Time!) {
- project(fullPath: $path) {
- milestones(timeframe: { start: $tstart, end: $tend }, startDate: $start, endDate: $end) {
- nodes { id }
- }
- }
- }
- GQL
-
- post_graphql(query, current_user: current_user,
- variables: vars.merge(vars.transform_keys { |k| :"t#{k}" }))
-
- expect(graphql_errors).to contain_exactly(a_hash_including('message' => include('deprecated in favor of timeframe')))
- end
-
it 'is invalid to invert the timeframe arguments' do
query = <<~GQL
query($path: ID!, $start: Date!, $end: Date!) {
diff --git a/spec/requests/api/graphql/project/project_statistics_redirect_spec.rb b/spec/requests/api/graphql/project/project_statistics_redirect_spec.rb
new file mode 100644
index 00000000000..8049a75ace3
--- /dev/null
+++ b/spec/requests/api/graphql/project/project_statistics_redirect_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'rendering project storage type routes', feature_category: :shared do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:user) { create(:user) }
+
+ let(:query) do
+ graphql_query_for('project',
+ { 'fullPath' => project.full_path },
+ "statisticsDetailsPaths { #{all_graphql_fields_for('ProjectStatisticsRedirect')} }")
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: user)
+ end
+ end
+
+ shared_examples 'valid routes for storage type' do
+ it 'contains all keys' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data['project']['statisticsDetailsPaths'].keys).to match_array(
+ %w[repository buildArtifacts wiki packages snippets containerRegistry]
+ )
+ end
+
+ it 'contains valid paths' do
+ repository_url = Gitlab::Routing.url_helpers.project_tree_url(project, "master")
+ wiki_url = Gitlab::Routing.url_helpers.project_wikis_pages_url(project)
+ build_artifacts_url = Gitlab::Routing.url_helpers.project_artifacts_url(project)
+ packages_url = Gitlab::Routing.url_helpers.project_packages_url(project)
+ snippets_url = Gitlab::Routing.url_helpers.project_snippets_url(project)
+ container_registry_url = Gitlab::Routing.url_helpers.project_container_registry_index_url(project)
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data['project']['statisticsDetailsPaths'].values).to match_array [repository_url,
+ wiki_url,
+ build_artifacts_url,
+ packages_url,
+ snippets_url,
+ container_registry_url]
+ end
+ end
+
+ context 'when project is public' do
+ it_behaves_like 'valid routes for storage type'
+
+ context 'when user is nil' do
+ let_it_be(:user) { nil }
+
+ it_behaves_like 'valid routes for storage type'
+ end
+ end
+
+ context 'when project is private' do
+ let_it_be(:project) { create(:project, :private) }
+
+ before do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like 'valid routes for storage type'
+
+ context 'when user is nil' do
+ it 'hides statisticsDetailsPaths for nil users' do
+ post_graphql(query, current_user: nil)
+
+ expect(graphql_data['project']).to be_blank
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb
index 477388585ca..8d4a39d6b30 100644
--- a/spec/requests/api/graphql/project/release_spec.rb
+++ b/spec/requests/api/graphql/project/release_spec.rb
@@ -132,7 +132,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
let(:release_fields) do
query_graphql_field(:assets, nil,
- query_graphql_field(:links, nil, 'nodes { id name url external, directAssetUrl }'))
+ query_graphql_field(:links, nil, 'nodes { id name url, directAssetUrl }'))
end
it 'finds all release links' do
@@ -141,7 +141,6 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
expected = release.links.map do |link|
a_graphql_entity_for(
link, :name, :url,
- 'external' => link.external?,
'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url
)
end
@@ -322,16 +321,15 @@ RSpec.describe 'Query.project(fullPath).release(tagName)', feature_category: :re
let(:release_fields) do
query_graphql_field(:assets, nil,
- query_graphql_field(:links, nil, 'nodes { id name url external, directAssetUrl }'))
+ query_graphql_field(:links, nil, 'nodes { id name url, directAssetUrl }'))
end
- it 'finds all non source external release links' do
+ it 'finds all non source release links' do
post_query
expected = release.links.map do |link|
a_graphql_entity_for(
link, :name, :url,
- 'external' => true,
'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url
)
end
diff --git a/spec/requests/api/graphql/project/user_access_authorized_agents_spec.rb b/spec/requests/api/graphql/project/user_access_authorized_agents_spec.rb
new file mode 100644
index 00000000000..b8017171fd1
--- /dev/null
+++ b/spec/requests/api/graphql/project/user_access_authorized_agents_spec.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project.user_access_authorized_agents', feature_category: :deployment_management do
+ include GraphqlHelpers
+
+ let_it_be(:organization) { create(:group) }
+ let_it_be(:agent_management_project) { create(:project, :private, group: organization) }
+ let_it_be(:agent) { create(:cluster_agent, project: agent_management_project) }
+
+ let_it_be(:deployment_project) { create(:project, :private, group: organization) }
+ let_it_be(:deployment_developer) { create(:user).tap { |u| deployment_project.add_developer(u) } }
+ let_it_be(:deployment_reporter) { create(:user).tap { |u| deployment_project.add_reporter(u) } }
+
+ let(:user) { deployment_developer }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{deployment_project.full_path}") {
+ userAccessAuthorizedAgents {
+ nodes {
+ agent {
+ id
+ name
+ project {
+ name
+ }
+ }
+ config
+ }
+ }
+ }
+ }
+ )
+ end
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ context 'with project authorization' do
+ let!(:user_access) { create(:agent_user_access_project_authorization, agent: agent, project: deployment_project) }
+
+ it 'returns the authorized agent' do
+ authorized_agents = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes')
+
+ expect(authorized_agents.count).to eq(1)
+
+ authorized_agent = authorized_agents.first
+
+ expect(authorized_agent['agent']['id']).to eq(agent.to_global_id.to_s)
+ expect(authorized_agent['agent']['name']).to eq(agent.name)
+ expect(authorized_agent['config']).to eq({})
+ expect(authorized_agent['agent']['project']).to be_nil # User is not authorized to read other resources.
+ end
+
+ context 'when user is developer in the agent management project' do
+ before do
+ agent_management_project.add_developer(deployment_developer)
+ end
+
+ it 'returns the project information as well' do
+ authorized_agent = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes').first
+
+ expect(authorized_agent['agent']['project']['name']).to eq(agent_management_project.name)
+ end
+ end
+
+ context 'when user is reporter' do
+ let(:user) { deployment_reporter }
+
+ it 'returns nothing' do
+ expect(subject['data']['project']['userAccessAuthorizedAgents']).to be_nil
+ end
+ end
+ end
+
+ context 'with group authorization' do
+ let_it_be(:deployment_group) { create(:group, :private, parent: organization) }
+
+ let!(:user_access) { create(:agent_user_access_group_authorization, agent: agent, group: deployment_group) }
+
+ before_all do
+ deployment_group.add_developer(deployment_developer)
+ deployment_group.add_reporter(deployment_reporter)
+ end
+
+ it 'returns the authorized agent' do
+ authorized_agents = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes')
+
+ expect(authorized_agents.count).to eq(1)
+
+ authorized_agent = authorized_agents.first
+
+ expect(authorized_agent['agent']['id']).to eq(agent.to_global_id.to_s)
+ expect(authorized_agent['agent']['name']).to eq(agent.name)
+ expect(authorized_agent['config']).to eq({})
+ expect(authorized_agent['agent']['project']).to be_nil # User is not authorized to read other resources.
+ end
+
+ context 'when user is developer in the agent management project' do
+ before do
+ agent_management_project.add_developer(deployment_developer)
+ end
+
+ it 'returns the project information as well' do
+ authorized_agent = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes').first
+
+ expect(authorized_agent['agent']['project']['name']).to eq(agent_management_project.name)
+ end
+ end
+
+ context 'when user is reporter' do
+ let(:user) { deployment_reporter }
+
+ it 'returns nothing' do
+ expect(subject['data']['project']['userAccessAuthorizedAgents']).to be_nil
+ end
+ end
+ end
+
+ context 'when deployment project is not authorized to user_access to the agent' do
+ it 'returns empty' do
+ authorized_agents = subject.dig('data', 'project', 'userAccessAuthorizedAgents', 'nodes')
+
+ expect(authorized_agents).to be_empty
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb
index f49165a88ea..628a2117e9d 100644
--- a/spec/requests/api/graphql/project/work_items_spec.rb
+++ b/spec/requests/api/graphql/project/work_items_spec.rb
@@ -120,24 +120,55 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
end
context 'when querying WorkItemWidgetHierarchy' do
- let_it_be(:children) { create_list(:work_item, 3, :task, project: project) }
+ let_it_be(:children) { create_list(:work_item, 4, :task, project: project) }
let_it_be(:child_link1) { create(:parent_link, work_item_parent: item1, work_item: children[0]) }
+ let_it_be(:child_link2) { create(:parent_link, work_item_parent: item1, work_item: children[1]) }
let(:fields) do
<<~GRAPHQL
- nodes {
- widgets {
- type
- ... on WorkItemWidgetHierarchy {
- hasChildren
- parent { id }
- children { nodes { id } }
- }
+ nodes {
+ id
+ widgets {
+ type
+ ... on WorkItemWidgetHierarchy {
+ hasChildren
+ parent { id }
+ children { nodes { id } }
}
}
+ }
GRAPHQL
end
+ context 'with ordered children' do
+ let(:items_data) { graphql_data['project']['workItems']['nodes'] }
+ let(:work_item_data) { items_data.find { |item| item['id'] == item1.to_gid.to_s } }
+ let(:work_item_widget) { work_item_data["widgets"].find { |widget| widget.key?("children") } }
+ let(:children_ids) { work_item_widget.dig("children", "nodes").pluck("id") }
+
+ let(:first_child) { children[0].to_gid.to_s }
+ let(:second_child) { children[1].to_gid.to_s }
+
+ it 'returns children ordered by created_at by default' do
+ post_graphql(query, current_user: current_user)
+
+ expect(children_ids).to eq([first_child, second_child])
+ end
+
+ context 'when ordered by relative position' do
+ before do
+ child_link1.update!(relative_position: 20)
+ child_link2.update!(relative_position: 10)
+ end
+
+ it 'returns children in correct order' do
+ post_graphql(query, current_user: current_user)
+
+ expect(children_ids).to eq([second_child, first_child])
+ end
+ end
+ end
+
it 'executes limited number of N+1 queries' do
post_graphql(query, current_user: current_user) # warm-up
@@ -146,13 +177,11 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
end
parent_work_items = create_list(:work_item, 2, project: project)
- create(:parent_link, work_item_parent: parent_work_items[0], work_item: children[1])
- create(:parent_link, work_item_parent: parent_work_items[1], work_item: children[2])
+ create(:parent_link, work_item_parent: parent_work_items[0], work_item: children[2])
+ create(:parent_link, work_item_parent: parent_work_items[1], work_item: children[3])
- # There are 2 extra queries for fetching the children field
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/363569
expect { post_graphql(query, current_user: current_user) }
- .not_to exceed_query_limit(control).with_threshold(2)
+ .not_to exceed_query_limit(control)
end
it 'avoids N+1 queries when children are added to a work item' do
@@ -162,8 +191,8 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
post_graphql(query, current_user: current_user)
end
- create(:parent_link, work_item_parent: item1, work_item: children[1])
create(:parent_link, work_item_parent: item1, work_item: children[2])
+ create(:parent_link, work_item_parent: item1, work_item: children[3])
expect { post_graphql(query, current_user: current_user) }
.not_to exceed_query_limit(control)
@@ -313,6 +342,79 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
end
end
+ context 'when fetching work item notifications widget' do
+ let(:fields) do
+ <<~GRAPHQL
+ nodes {
+ widgets {
+ type
+ ... on WorkItemWidgetNotifications {
+ subscribed
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'executes limited number of N+1 queries', :use_sql_query_cache do
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: current_user)
+ end
+
+ create_list(:work_item, 3, project: project)
+
+ # Performs 1 extra query per item to fetch subscriptions
+ expect { post_graphql(query, current_user: current_user) }
+ .not_to exceed_all_query_limit(control).with_threshold(3)
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'when fetching work item award emoji widget' do
+ let(:fields) do
+ <<~GRAPHQL
+ nodes {
+ widgets {
+ type
+ ... on WorkItemWidgetAwardEmoji {
+ awardEmoji {
+ nodes {
+ name
+ emoji
+ user { id }
+ }
+ }
+ upvotes
+ downvotes
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ before do
+ create(:award_emoji, name: 'star', user: current_user, awardable: item1)
+ create(:award_emoji, :upvote, awardable: item1)
+ create(:award_emoji, :downvote, awardable: item1)
+ end
+
+ it 'executes limited number of N+1 queries', :use_sql_query_cache do
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: current_user)
+ end
+
+ create_list(:work_item, 2, project: project) do |item|
+ create(:award_emoji, name: 'rocket', awardable: item)
+ create_list(:award_emoji, 2, :upvote, awardable: item)
+ create_list(:award_emoji, 2, :downvote, awardable: item)
+ end
+
+ expect { post_graphql(query, current_user: current_user) }
+ .not_to exceed_all_query_limit(control)
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
def item_ids
graphql_dig_at(items_data, :node, :id)
end
diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb
index 281a08e6548..9f51258c163 100644
--- a/spec/requests/api/graphql/project_query_spec.rb
+++ b/spec/requests/api/graphql/project_query_spec.rb
@@ -120,6 +120,67 @@ RSpec.describe 'getting project information', feature_category: :projects do
end
end
+ describe 'is_catalog_resource' do
+ before do
+ project.add_owner(current_user)
+ end
+
+ let(:catalog_resource_query) do
+ <<~GRAPHQL
+ {
+ project(fullPath: "#{project.full_path}") {
+ isCatalogResource
+ }
+ }
+ GRAPHQL
+ end
+
+ context 'when the project is not a catalog resource' do
+ it 'is false' do
+ post_graphql(catalog_resource_query, current_user: current_user)
+
+ expect(graphql_data.dig('project', 'isCatalogResource')).to be(false)
+ end
+ end
+
+ context 'when the project is a catalog resource' do
+ before do
+ create(:catalog_resource, project: project)
+ end
+
+ it 'is true' do
+ post_graphql(catalog_resource_query, current_user: current_user)
+
+ expect(graphql_data.dig('project', 'isCatalogResource')).to be(true)
+ end
+ end
+
+ context 'for N+1 queries with isCatalogResource' do
+ let_it_be(:project1) { create(:project, group: group) }
+ let_it_be(:project2) { create(:project, group: group) }
+
+ it 'avoids N+1 database queries' do
+ pending('See: https://gitlab.com/gitlab-org/gitlab/-/issues/403634')
+ ctx = { current_user: current_user }
+
+ baseline_query = graphql_query_for(:project, { full_path: project1.full_path }, 'isCatalogResource')
+
+ query = <<~GQL
+ query {
+ a: #{query_graphql_field(:project, { full_path: project1.full_path }, 'isCatalogResource')}
+ b: #{query_graphql_field(:project, { full_path: project2.full_path }, 'isCatalogResource')}
+ }
+ GQL
+
+ control = ActiveRecord::QueryRecorder.new do
+ run_with_clean_state(baseline_query, context: ctx)
+ end
+
+ expect { run_with_clean_state(query, context: ctx) }.not_to exceed_query_limit(control)
+ end
+ end
+ end
+
context 'when the user has reporter access to the project' do
let(:statistics_query) do
<<~GRAPHQL
diff --git a/spec/requests/api/graphql/query_spec.rb b/spec/requests/api/graphql/query_spec.rb
index 2b9d66ec744..0602cfec149 100644
--- a/spec/requests/api/graphql/query_spec.rb
+++ b/spec/requests/api/graphql/query_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe 'Query', feature_category: :not_owned do
+RSpec.describe 'Query', feature_category: :shared do
include GraphqlHelpers
- let_it_be(:project) { create(:project) }
+ let_it_be(:project) { create(:project, public_builds: false) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:developer) { create(:user) }
@@ -116,4 +116,36 @@ RSpec.describe 'Query', feature_category: :not_owned do
end
end
end
+
+ describe '.ciPipelineStage' do
+ let_it_be(:ci_stage) { create(:ci_stage, name: 'graphql test stage', project: project) }
+
+ let(:query) do
+ <<~GRAPHQL
+ {
+ ciPipelineStage(id: "#{ci_stage.to_global_id}") {
+ name
+ }
+ }
+ GRAPHQL
+ end
+
+ context 'when the current user has access to the stage' do
+ it 'fetches the stage for the given ID' do
+ project.add_developer(developer)
+
+ post_graphql(query, current_user: developer)
+
+ expect(graphql_data.dig('ciPipelineStage', 'name')).to eq('graphql test stage')
+ end
+ end
+
+ context 'when the current user does not have access to the stage' do
+ it 'returns nil' do
+ post_graphql(query, current_user: developer)
+
+ expect(graphql_data['ciPipelineStage']).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/user/user_achievements_query_spec.rb b/spec/requests/api/graphql/user/user_achievements_query_spec.rb
new file mode 100644
index 00000000000..27d32d07372
--- /dev/null
+++ b/spec/requests/api/graphql/user/user_achievements_query_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'UserAchievements', feature_category: :user_profile do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:achievement) { create(:achievement, namespace: group) }
+ let_it_be(:non_revoked_achievement) { create(:user_achievement, achievement: achievement, user: user) }
+ let_it_be(:revoked_achievement) { create(:user_achievement, :revoked, achievement: achievement, user: user) }
+ let_it_be(:fields) do
+ <<~HEREDOC
+ userAchievements {
+ nodes {
+ id
+ achievement {
+ id
+ }
+ user {
+ id
+ }
+ awardedByUser {
+ id
+ }
+ revokedByUser {
+ id
+ }
+ }
+ }
+ HEREDOC
+ end
+
+ let_it_be(:query) do
+ graphql_query_for('user', { id: user.to_global_id.to_s }, fields)
+ end
+
+ let(:current_user) { user }
+
+ before_all do
+ group.add_guest(user)
+ end
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns all non_revoked user_achievements' do
+ expect(graphql_data_at(:user, :userAchievements, :nodes)).to contain_exactly(
+ a_graphql_entity_for(non_revoked_achievement)
+ )
+ end
+
+ it 'can lookahead to eliminate N+1 queries', :use_clean_rails_memory_store_caching do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: user)
+ end.count
+
+ achievement2 = create(:achievement, namespace: group)
+ create_list(:user_achievement, 2, achievement: achievement2, user: user)
+
+ expect { post_graphql(query, current_user: user) }.not_to exceed_all_query_limit(control_count)
+ end
+
+ context 'when the achievements feature flag is disabled for a namespace' do
+ let_it_be(:group2) { create(:group) }
+ let_it_be(:achievement2) { create(:achievement, namespace: group2) }
+ let_it_be(:user_achievement2) { create(:user_achievement, achievement: achievement2, user: user) }
+
+ before do
+ stub_feature_flags(achievements: false)
+ stub_feature_flags(achievements: group2)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'does not return user_achievements for that namespace' do
+ expect(graphql_data_at(:user, :userAchievements, :nodes)).to contain_exactly(
+ a_graphql_entity_for(user_achievement2)
+ )
+ end
+ end
+
+ context 'when current user is not a member of the private group' do
+ let(:current_user) { create(:user) }
+
+ it 'returns all achievements' do
+ expect(graphql_data_at(:user, :userAchievements, :nodes)).to contain_exactly(
+ a_graphql_entity_for(non_revoked_achievement)
+ )
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/user_spec.rb b/spec/requests/api/graphql/user_spec.rb
index c19dfa6f3f3..41ee233dfc5 100644
--- a/spec/requests/api/graphql/user_spec.rb
+++ b/spec/requests/api/graphql/user_spec.rb
@@ -10,6 +10,12 @@ RSpec.describe 'User', feature_category: :user_profile do
shared_examples 'a working user query' do
it_behaves_like 'a working graphql query' do
before do
+ # TODO: This license stub is necessary because the remote development workspaces field
+ # defined in the EE version of UserInterface gets picked up here and thus the license
+ # check happens. This comes from the `ancestors` call in
+ # lib/graphql/schema/member/has_fields.rb#fields in the graphql library.
+ stub_licensed_features(remote_development: true)
+
post_graphql(query, current_user: current_user)
end
end
@@ -36,9 +42,17 @@ RSpec.describe 'User', feature_category: :user_profile do
end
context 'when username parameter is used' do
- let(:query) { graphql_query_for(:user, { username: current_user.username.to_s }) }
+ context 'when username is identically cased' do
+ let(:query) { graphql_query_for(:user, { username: current_user.username.to_s }) }
- it_behaves_like 'a working user query'
+ it_behaves_like 'a working user query'
+ end
+
+ context 'when username is differently cased' do
+ let(:query) { graphql_query_for(:user, { username: current_user.username.to_s.upcase }) }
+
+ it_behaves_like 'a working user query'
+ end
end
context 'when username and id parameter are used' do
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index 0fad4f4ff3a..dc5004a121b 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -36,9 +36,15 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
end
context 'when the user can read the work item' do
+ let(:incoming_email_token) { current_user.incoming_email_token }
+ let(:work_item_email) do
+ "p+#{project.full_path_slug}-#{project.project_id}-#{incoming_email_token}-issue-#{work_item.iid}@gl.ab"
+ end
+
before do
project.add_developer(developer)
project.add_guest(guest)
+ stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
post_graphql(query, current_user: current_user)
end
@@ -55,11 +61,15 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
'title' => work_item.title,
'confidential' => work_item.confidential,
'workItemType' => hash_including('id' => work_item.work_item_type.to_gid.to_s),
+ 'reference' => work_item.to_reference,
+ 'createNoteEmail' => work_item_email,
'userPermissions' => {
'readWorkItem' => true,
'updateWorkItem' => true,
'deleteWorkItem' => false,
- 'adminWorkItem' => true
+ 'adminWorkItem' => true,
+ 'adminParentLink' => true,
+ 'setWorkItemMetadata' => true
},
'project' => hash_including('id' => project.to_gid.to_s, 'fullPath' => project.full_path)
)
@@ -373,6 +383,161 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
)
end
end
+
+ describe 'notifications widget' do
+ let(:work_item_fields) do
+ <<~GRAPHQL
+ id
+ widgets {
+ type
+ ... on WorkItemWidgetNotifications {
+ subscribed
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'returns widget information' do
+ expect(work_item_data).to include(
+ 'id' => work_item.to_gid.to_s,
+ 'widgets' => include(
+ hash_including(
+ 'type' => 'NOTIFICATIONS',
+ 'subscribed' => work_item.subscribed?(current_user, project)
+ )
+ )
+ )
+ end
+ end
+
+ describe 'currentUserTodos widget' do
+ let_it_be(:current_user) { developer }
+ let_it_be(:other_todo) { create(:todo, state: :pending, user: current_user) }
+
+ let_it_be(:done_todo) do
+ create(:todo, state: :done, target: work_item, target_type: work_item.class.name, user: current_user)
+ end
+
+ let_it_be(:pending_todo) do
+ create(:todo, state: :pending, target: work_item, target_type: work_item.class.name, user: current_user)
+ end
+
+ let_it_be(:other_user_todo) do
+ create(:todo, state: :pending, target: work_item, target_type: work_item.class.name, user: create(:user))
+ end
+
+ let(:work_item_fields) do
+ <<~GRAPHQL
+ id
+ widgets {
+ type
+ ... on WorkItemWidgetCurrentUserTodos {
+ currentUserTodos {
+ nodes {
+ id
+ state
+ }
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ context 'with access' do
+ it 'returns widget information' do
+ expect(work_item_data).to include(
+ 'id' => work_item.to_gid.to_s,
+ 'widgets' => include(
+ hash_including(
+ 'type' => 'CURRENT_USER_TODOS',
+ 'currentUserTodos' => {
+ 'nodes' => match_array(
+ [done_todo, pending_todo].map { |t| { 'id' => t.to_gid.to_s, 'state' => t.state } }
+ )
+ }
+ )
+ )
+ )
+ end
+ end
+
+ context 'with filter' do
+ let(:work_item_fields) do
+ <<~GRAPHQL
+ id
+ widgets {
+ type
+ ... on WorkItemWidgetCurrentUserTodos {
+ currentUserTodos(state: done) {
+ nodes {
+ id
+ state
+ }
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'returns widget information' do
+ expect(work_item_data).to include(
+ 'id' => work_item.to_gid.to_s,
+ 'widgets' => include(
+ hash_including(
+ 'type' => 'CURRENT_USER_TODOS',
+ 'currentUserTodos' => {
+ 'nodes' => match_array(
+ [done_todo].map { |t| { 'id' => t.to_gid.to_s, 'state' => t.state } }
+ )
+ }
+ )
+ )
+ )
+ end
+ end
+ end
+
+ describe 'award emoji widget' do
+ let_it_be(:emoji) { create(:award_emoji, name: 'star', awardable: work_item) }
+ let_it_be(:upvote) { create(:award_emoji, :upvote, awardable: work_item) }
+ let_it_be(:downvote) { create(:award_emoji, :downvote, awardable: work_item) }
+
+ let(:work_item_fields) do
+ <<~GRAPHQL
+ id
+ widgets {
+ type
+ ... on WorkItemWidgetAwardEmoji {
+ upvotes
+ downvotes
+ awardEmoji {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ it 'returns widget information' do
+ expect(work_item_data).to include(
+ 'id' => work_item.to_gid.to_s,
+ 'widgets' => include(
+ hash_including(
+ 'type' => 'AWARD_EMOJI',
+ 'upvotes' => work_item.upvotes,
+ 'downvotes' => work_item.downvotes,
+ 'awardEmoji' => {
+ 'nodes' => match_array(
+ [emoji, upvote, downvote].map { |e| { 'name' => e.name } }
+ )
+ }
+ )
+ )
+ )
+ end
+ end
end
context 'when an Issue Global ID is provided' do
@@ -398,4 +563,23 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
)
end
end
+
+ context 'when the user cannot set work item metadata' do
+ let(:current_user) { guest }
+
+ before do
+ project.add_guest(guest)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'returns correct user permission' do
+ expect(work_item_data).to include(
+ 'id' => work_item.to_gid.to_s,
+ 'userPermissions' =>
+ hash_including(
+ 'setWorkItemMetadata' => false
+ )
+ )
+ end
+ end
end
diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb
index d7724371cce..8a3c5261eb6 100644
--- a/spec/requests/api/graphql_spec.rb
+++ b/spec/requests/api/graphql_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'GraphQL', feature_category: :not_owned do
+RSpec.describe 'GraphQL', feature_category: :shared do
include GraphqlHelpers
include AfterNextHelpers
diff --git a/spec/requests/api/group_clusters_spec.rb b/spec/requests/api/group_clusters_spec.rb
index 68c3af01e56..58d0e6a1eb5 100644
--- a/spec/requests/api/group_clusters_spec.rb
+++ b/spec/requests/api/group_clusters_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::GroupClusters, feature_category: :kubernetes_management do
+RSpec.describe API::GroupClusters, feature_category: :deployment_management do
include KubernetesHelpers
let(:current_user) { create(:user) }
diff --git a/spec/requests/api/group_milestones_spec.rb b/spec/requests/api/group_milestones_spec.rb
index 91f64d02d43..2f05b0fcf21 100644
--- a/spec/requests/api/group_milestones_spec.rb
+++ b/spec/requests/api/group_milestones_spec.rb
@@ -4,35 +4,41 @@ require 'spec_helper'
RSpec.describe API::GroupMilestones, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group, :private) }
+ let_it_be_with_refind(:group) { create(:group, :private) }
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:group_member) { create(:group_member, group: group, user: user) }
- let_it_be(:closed_milestone) { create(:closed_milestone, group: group, title: 'version1', description: 'closed milestone') }
- let_it_be(:milestone) { create(:milestone, group: group, title: 'version2', description: 'open milestone') }
+ let_it_be(:closed_milestone) do
+ create(:closed_milestone, group: group, title: 'version1', description: 'closed milestone')
+ end
+
+ let_it_be_with_reload(:milestone) do
+ create(:milestone, group: group, title: 'version2', description: 'open milestone', updated_at: 4.days.ago)
+ end
let(:route) { "/groups/#{group.id}/milestones" }
+ shared_examples 'listing all milestones' do
+ it 'returns correct list of milestones' do
+ get api(route, user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.size).to eq(milestones.size)
+ expect(json_response.map { |entry| entry["id"] }).to eq(milestones.map(&:id))
+ end
+ end
+
it_behaves_like 'group and project milestones', "/groups/:id/milestones"
describe 'GET /groups/:id/milestones' do
- context 'when include_parent_milestones is true' do
- let_it_be(:ancestor_group) { create(:group, :private) }
- let_it_be(:ancestor_group_milestone) { create(:milestone, group: ancestor_group) }
- let_it_be(:params) { { include_parent_milestones: true } }
-
- before_all do
- group.update!(parent: ancestor_group)
- end
+ let_it_be(:ancestor_group) { create(:group, :private) }
+ let_it_be(:ancestor_group_milestone) { create(:milestone, group: ancestor_group, updated_at: 2.days.ago) }
- shared_examples 'listing all milestones' do
- it 'returns correct list of milestones' do
- get api(route, user), params: params
+ before_all do
+ group.update!(parent: ancestor_group)
+ end
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to eq(milestones.size)
- expect(json_response.map { |entry| entry["id"] }).to eq(milestones.map(&:id))
- end
- end
+ context 'when include_parent_milestones is true' do
+ let(:params) { { include_parent_milestones: true } }
context 'when user has access to ancestor groups' do
let(:milestones) { [ancestor_group_milestone, milestone, closed_milestone] }
@@ -45,10 +51,26 @@ RSpec.describe API::GroupMilestones, feature_category: :team_planning do
it_behaves_like 'listing all milestones'
context 'when iids param is present' do
- let_it_be(:params) { { include_parent_milestones: true, iids: [milestone.iid] } }
+ let(:params) { { include_parent_milestones: true, iids: [milestone.iid] } }
it_behaves_like 'listing all milestones'
end
+
+ context 'when updated_before param is present' do
+ let(:params) { { updated_before: 1.day.ago.iso8601, include_parent_milestones: true } }
+
+ it_behaves_like 'listing all milestones' do
+ let(:milestones) { [ancestor_group_milestone, milestone] }
+ end
+ end
+
+ context 'when updated_after param is present' do
+ let(:params) { { updated_after: 1.day.ago.iso8601, include_parent_milestones: true } }
+
+ it_behaves_like 'listing all milestones' do
+ let(:milestones) { [closed_milestone] }
+ end
+ end
end
context 'when user has no access to ancestor groups' do
@@ -63,6 +85,22 @@ RSpec.describe API::GroupMilestones, feature_category: :team_planning do
end
end
end
+
+ context 'when updated_before param is present' do
+ let(:params) { { updated_before: 1.day.ago.iso8601 } }
+
+ it_behaves_like 'listing all milestones' do
+ let(:milestones) { [milestone] }
+ end
+ end
+
+ context 'when updated_after param is present' do
+ let(:params) { { updated_after: 1.day.ago.iso8601 } }
+
+ it_behaves_like 'listing all milestones' do
+ let(:milestones) { [closed_milestone] }
+ end
+ end
end
describe 'GET /groups/:id/milestones/:milestone_id/issues' do
diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb
index e3d538d72ba..6849b087211 100644
--- a/spec/requests/api/group_variables_spec.rb
+++ b/spec/requests/api/group_variables_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::GroupVariables, feature_category: :pipeline_authoring do
+RSpec.describe API::GroupVariables, feature_category: :secrets_management do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let_it_be(:variable) { create(:ci_group_variable, group: group) }
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 12a6553f51a..84d48b4edb4 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
include GroupAPIHelpers
include UploadHelpers
include WorkhorseHelpers
+ include KeysetPaginationHelpers
let_it_be(:user1) { create(:user, can_create_group: false) }
let_it_be(:user2) { create(:user) }
@@ -39,7 +40,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
context 'when invalid' do
shared_examples 'invalid file upload request' do
- it 'returns 400' do
+ it 'returns 400', :aggregate_failures do
make_upload_request
expect(response).to have_gitlab_http_status(:bad_request)
@@ -65,7 +66,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
shared_examples 'skips searching in full path' do
- it 'does not find groups by full path' do
+ it 'does not find groups by full path', :aggregate_failures do
subgroup = create(:group, parent: parent, path: "#{parent.path}-subgroup")
create(:group, parent: parent, path: 'not_matching_path')
@@ -79,7 +80,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
describe "GET /groups" do
context "when unauthenticated" do
- it "returns public groups" do
+ it "returns public groups", :aggregate_failures do
get api("/groups")
expect(response).to have_gitlab_http_status(:ok)
@@ -93,18 +94,18 @@ RSpec.describe API::Groups, feature_category: :subgroups do
it 'avoids N+1 queries', :use_sql_query_cache do
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api("/groups", admin)
+ get api("/groups")
end
create(:group)
expect do
- get api("/groups", admin)
+ get api("/groups")
end.not_to exceed_all_query_limit(control)
end
context 'when statistics are requested' do
- it 'does not include statistics' do
+ it 'does not include statistics', :aggregate_failures do
get api("/groups"), params: { statistics: true }
expect(response).to have_gitlab_http_status(:ok)
@@ -116,7 +117,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context "when authenticated as user" do
- it "normal user: returns an array of groups of user1" do
+ it "normal user: returns an array of groups of user1", :aggregate_failures do
get api("/groups", user1)
expect(response).to have_gitlab_http_status(:ok)
@@ -127,7 +128,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
.to satisfy_one { |group| group['name'] == group1.name }
end
- it "does not include runners_token information" do
+ it "does not include runners_token information", :aggregate_failures do
get api("/groups", user1)
expect(response).to have_gitlab_http_status(:ok)
@@ -137,7 +138,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.first).not_to include('runners_token')
end
- it "does not include statistics" do
+ it "does not include statistics", :aggregate_failures do
get api("/groups", user1), params: { statistics: true }
expect(response).to have_gitlab_http_status(:ok)
@@ -146,7 +147,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.first).not_to include 'statistics'
end
- it "includes a created_at timestamp" do
+ it "includes a created_at timestamp", :aggregate_failures do
get api("/groups", user1)
expect(response).to have_gitlab_http_status(:ok)
@@ -175,7 +176,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'on making requests below the allowed offset pagination threshold' do
- it 'paginates the records' do
+ it 'paginates the records', :aggregate_failures do
get api('/groups'), params: { page: 1, per_page: 1 }
expect(response).to have_gitlab_http_status(:ok)
@@ -196,25 +197,8 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'keyset pagination' do
- def pagination_links(response)
- link = response.headers['LINK']
- return unless link
-
- link.split(',').map do |link|
- match = link.match(/<(?<url>.*)>; rel="(?<rel>\w+)"/)
- break nil unless match
-
- { url: match[:url], rel: match[:rel] }
- end.compact
- end
-
- def params_for_next_page(response)
- next_url = pagination_links(response).find { |link| link[:rel] == 'next' }[:url]
- Rack::Utils.parse_query(URI.parse(next_url).query)
- end
-
context 'on making requests with supported ordering structure' do
- it 'paginates the records correctly' do
+ it 'paginates the records correctly', :aggregate_failures do
# first page
get api('/groups'), params: { pagination: 'keyset', per_page: 1 }
@@ -223,7 +207,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(records.size).to eq(1)
expect(records.first['id']).to eq(group_1.id)
- params_for_next_page = params_for_next_page(response)
+ params_for_next_page = pagination_params_from_next_url(response)
expect(params_for_next_page).to include('cursor')
get api('/groups'), params: params_for_next_page
@@ -236,7 +220,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'on making requests with unsupported ordering structure' do
- it 'returns error' do
+ it 'returns error', :aggregate_failures do
get api('/groups'), params: { pagination: 'keyset', per_page: 1, order_by: 'path', sort: 'desc' }
expect(response).to have_gitlab_http_status(:method_not_allowed)
@@ -248,8 +232,8 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context "when authenticated as admin" do
- it "admin: returns an array of all groups" do
- get api("/groups", admin)
+ it "admin: returns an array of all groups", :aggregate_failures do
+ get api("/groups", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -257,8 +241,8 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.length).to eq(2)
end
- it "does not include runners_token information" do
- get api("/groups", admin)
+ it "does not include runners_token information", :aggregate_failures do
+ get api("/groups", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -267,8 +251,8 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.first).not_to include('runners_token')
end
- it "does not include statistics by default" do
- get api("/groups", admin)
+ it "does not include statistics by default", :aggregate_failures do
+ get api("/groups", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -276,8 +260,8 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.first).not_to include('statistics')
end
- it "includes a created_at timestamp" do
- get api("/groups", admin)
+ it "includes a created_at timestamp", :aggregate_failures do
+ get api("/groups", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -285,7 +269,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.first['created_at']).to be_present
end
- it "includes statistics if requested" do
+ it "includes statistics if requested", :aggregate_failures do
attributes = {
storage_size: 4093,
repository_size: 123,
@@ -302,7 +286,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
project1.statistics.update!(attributes)
- get api("/groups", admin), params: { statistics: true }
+ get api("/groups", admin, admin_mode: true), params: { statistics: true }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -313,8 +297,8 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context "when using skip_groups in request" do
- it "returns all groups excluding skipped groups" do
- get api("/groups", admin), params: { skip_groups: [group2.id] }
+ it "returns all groups excluding skipped groups", :aggregate_failures do
+ get api("/groups", admin, admin_mode: true), params: { skip_groups: [group2.id] }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -326,7 +310,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
context "when using all_available in request" do
let(:response_groups) { json_response.map { |group| group['name'] } }
- it "returns all groups you have access to" do
+ it "returns all groups you have access to", :aggregate_failures do
public_group = create :group, :public
get api("/groups", user1), params: { all_available: true }
@@ -348,7 +332,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
subgroup.add_owner(user1)
end
- it "doesn't return subgroups" do
+ it "doesn't return subgroups", :aggregate_failures do
get api("/groups", user1), params: { top_level_only: true }
expect(response).to have_gitlab_http_status(:ok)
@@ -373,7 +357,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
group5.add_owner(user1)
end
- it "sorts by name ascending by default" do
+ it "sorts by name ascending by default", :aggregate_failures do
get api("/groups", user1)
expect(response).to have_gitlab_http_status(:ok)
@@ -382,7 +366,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(response_groups).to eq(groups_visible_to_user(user1).order(:name).pluck(:name))
end
- it "sorts in descending order when passed" do
+ it "sorts in descending order when passed", :aggregate_failures do
get api("/groups", user1), params: { sort: "desc" }
expect(response).to have_gitlab_http_status(:ok)
@@ -391,7 +375,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(response_groups).to eq(groups_visible_to_user(user1).order(name: :desc).pluck(:name))
end
- it "sorts by path in order_by param" do
+ it "sorts by path in order_by param", :aggregate_failures do
get api("/groups", user1), params: { order_by: "path" }
expect(response).to have_gitlab_http_status(:ok)
@@ -400,7 +384,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(response_groups).to eq(groups_visible_to_user(user1).order(:path).pluck(:name))
end
- it "sorts by id in the order_by param" do
+ it "sorts by id in the order_by param", :aggregate_failures do
get api("/groups", user1), params: { order_by: "id" }
expect(response).to have_gitlab_http_status(:ok)
@@ -409,7 +393,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(response_groups).to eq(groups_visible_to_user(user1).order(:id).pluck(:name))
end
- it "sorts also by descending id with pagination fix" do
+ it "sorts also by descending id with pagination fix", :aggregate_failures do
get api("/groups", user1), params: { order_by: "id", sort: "desc" }
expect(response).to have_gitlab_http_status(:ok)
@@ -418,7 +402,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(response_groups).to eq(groups_visible_to_user(user1).order(id: :desc).pluck(:name))
end
- it "sorts identical keys by id for good pagination" do
+ it "sorts identical keys by id for good pagination", :aggregate_failures do
get api("/groups", user1), params: { search: "same-name", order_by: "name" }
expect(response).to have_gitlab_http_status(:ok)
@@ -427,7 +411,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(response_groups_ids).to eq(Group.select { |group| group['name'] == 'same-name' }.map { |group| group['id'] }.sort)
end
- it "sorts descending identical keys by id for good pagination" do
+ it "sorts descending identical keys by id for good pagination", :aggregate_failures do
get api("/groups", user1), params: { search: "same-name", order_by: "name", sort: "desc" }
expect(response).to have_gitlab_http_status(:ok)
@@ -449,7 +433,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
subject { get api('/groups', user1), params: params }
- it 'sorts top level groups before subgroups with exact matches first' do
+ it 'sorts top level groups before subgroups with exact matches first', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -462,7 +446,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
context 'when `search` parameter is not given' do
let(:params) { { order_by: 'similarity' } }
- it 'sorts items ordered by name' do
+ it 'sorts items ordered by name', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -480,7 +464,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'when using owned in the request' do
- it 'returns an array of groups the user owns' do
+ it 'returns an array of groups the user owns', :aggregate_failures do
group1.add_maintainer(user2)
get api('/groups', user2), params: { owned: true }
@@ -503,7 +487,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'with min_access_level parameter' do
- it 'returns an array of groups the user has at least master access' do
+ it 'returns an array of groups the user has at least master access', :aggregate_failures do
get api('/groups', user2), params: { min_access_level: 40 }
expect(response).to have_gitlab_http_status(:ok)
@@ -512,24 +496,15 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(response_groups).to contain_exactly(group2.id, group3.id)
end
- context 'distinct count with present_groups_select_all feature flag' do
+ context 'distinct count' do
subject { get api('/groups', user2), params: { min_access_level: 40 } }
+ # Prevent Rails from optimizing the count query and inadvertadly creating a poor performing databse query.
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/368969
it 'counts with *' do
count_sql = /#{Regexp.escape('SELECT count(*)')}/i
expect { subject }.to make_queries_matching count_sql
end
-
- context 'when present_groups_select_all feature flag is disabled' do
- before do
- stub_feature_flags(present_groups_select_all: false)
- end
-
- it 'counts with count_column' do
- count_sql = /#{Regexp.escape('SELECT count(count_column)')}/i
- expect { subject }.to make_queries_matching count_sql
- end
- end
end
end
end
@@ -541,7 +516,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
subject { get api('/groups', user1), params: { search: group1.path } }
- it 'finds also groups with full path matching search param' do
+ it 'finds also groups with full path matching search param', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -587,7 +562,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(response).to have_gitlab_http_status(:not_found)
end
- it 'returns 200 for a public group' do
+ it 'returns 200 for a public group', :aggregate_failures do
get api("/groups/#{group1.id}")
expect(response).to have_gitlab_http_status(:ok)
@@ -617,7 +592,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context "when authenticated as user" do
- it "returns one of user1's groups" do
+ it "returns one of user1's groups", :aggregate_failures do
project = create(:project, namespace: group2, path: 'Foo')
create(:project_group_link, project: project, group: group1)
group = create(:group)
@@ -661,7 +636,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response['shared_projects'][0]['id']).to eq(project.id)
end
- it "returns one of user1's groups without projects when with_projects option is set to false" do
+ it "returns one of user1's groups without projects when with_projects option is set to false", :aggregate_failures do
project = create(:project, namespace: group2, path: 'Foo')
create(:project_group_link, project: project, group: group1)
@@ -673,14 +648,14 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response).not_to include('runners_token')
end
- it "doesn't return runners_token if the user is not the owner of the group" do
+ it "doesn't return runners_token if the user is not the owner of the group", :aggregate_failures do
get api("/groups/#{group1.id}", user3)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).not_to include('runners_token')
end
- it "returns runners_token if the user is the owner of the group" do
+ it "returns runners_token if the user is the owner of the group", :aggregate_failures do
group1.add_owner(user3)
get api("/groups/#{group1.id}", user3)
@@ -720,8 +695,9 @@ RSpec.describe API::Groups, feature_category: :subgroups do
.to contain_exactly(projects[:public].id, projects[:internal].id)
end
- it 'avoids N+1 queries with project links' do
+ it 'avoids N+1 queries with project links', :aggregate_failures do
get api("/groups/#{group1.id}", user1)
+ expect(response).to have_gitlab_http_status(:ok)
control_count = ActiveRecord::QueryRecorder.new do
get api("/groups/#{group1.id}", user1)
@@ -754,25 +730,25 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context "when authenticated as admin" do
- it "returns any existing group" do
- get api("/groups/#{group2.id}", admin)
+ it "returns any existing group", :aggregate_failures do
+ get api("/groups/#{group2.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(group2.name)
end
- it "returns information of the runners_token for the group" do
- get api("/groups/#{group2.id}", admin)
+ it "returns information of the runners_token for the group", :aggregate_failures do
+ get api("/groups/#{group2.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to include('runners_token')
end
- it "returns runners_token and no projects when with_projects option is set to false" do
+ it "returns runners_token and no projects when with_projects option is set to false", :aggregate_failures do
project = create(:project, namespace: group2, path: 'Foo')
create(:project_group_link, project: project, group: group1)
- get api("/groups/#{group2.id}", admin), params: { with_projects: false }
+ get api("/groups/#{group2.id}", admin, admin_mode: true), params: { with_projects: false }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['projects']).to be_nil
@@ -781,14 +757,14 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
it "does not return a non existing group" do
- get api("/groups/#{non_existing_record_id}", admin)
+ get api("/groups/#{non_existing_record_id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when using group path in URL' do
- it 'returns any existing group' do
+ it 'returns any existing group', :aggregate_failures do
get api("/groups/#{group1.path}", admin)
expect(response).to have_gitlab_http_status(:ok)
@@ -796,7 +772,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
it 'does not return a non existing group' do
- get api('/groups/unknown', admin)
+ get api('/groups/unknown', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -826,7 +802,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
end
- it 'limits projects and shared_projects' do
+ it 'limits projects and shared_projects', :aggregate_failures do
get api("/groups/#{group1.id}")
expect(json_response['projects'].count).to eq(limit)
@@ -843,8 +819,8 @@ RSpec.describe API::Groups, feature_category: :subgroups do
subject(:shared_with_groups) { json_response['shared_with_groups'].map { _1['group_id']} }
context 'when authenticated as admin' do
- it 'returns all groups that share the group' do
- get api("/groups/#{shared_group.id}", admin)
+ it 'returns all groups that share the group', :aggregate_failures do
+ get api("/groups/#{shared_group.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(shared_with_groups).to contain_exactly(group_link_1.shared_with_group_id, group_link_2.shared_with_group_id)
@@ -852,7 +828,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'when unauthenticated' do
- it 'returns only public groups that share the group' do
+ it 'returns only public groups that share the group', :aggregate_failures do
get api("/groups/#{shared_group.id}")
expect(response).to have_gitlab_http_status(:ok)
@@ -861,7 +837,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'when authenticated as a member of a parent group that has shared the group' do
- it 'returns private group if direct member' do
+ it 'returns private group if direct member', :aggregate_failures do
group2_sub.add_guest(user3)
get api("/groups/#{shared_group.id}", user3)
@@ -870,7 +846,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(shared_with_groups).to contain_exactly(group_link_1.shared_with_group_id, group_link_2.shared_with_group_id)
end
- it 'returns private group if inherited member' do
+ it 'returns private group if inherited member', :aggregate_failures do
inherited_guest_member = create(:user)
group2.add_guest(inherited_guest_member)
@@ -902,7 +878,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'when authenticated as the group owner' do
- it 'updates the group' do
+ it 'updates the group', :aggregate_failures do
workhorse_form_with_file(
api("/groups/#{group1.id}", user1),
method: :put,
@@ -942,7 +918,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response['prevent_sharing_groups_outside_hierarchy']).to eq(true)
end
- it 'removes the group avatar' do
+ it 'removes the group avatar', :aggregate_failures do
put api("/groups/#{group1.id}", user1), params: { avatar: '' }
aggregate_failures "testing response" do
@@ -952,7 +928,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
end
- it 'does not update visibility_level if it is restricted' do
+ it 'does not update visibility_level if it is restricted', :aggregate_failures do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
put api("/groups/#{group1.id}", user1), params: { visibility: 'internal' }
@@ -967,7 +943,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'for users who have the ability to update default_branch_protection' do
- it 'updates the attribute' do
+ it 'updates the attribute', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -976,7 +952,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'for users who does not have the ability to update default_branch_protection`' do
- it 'does not update the attribute' do
+ it 'does not update the attribute', :aggregate_failures do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(user1, :update_default_branch_protection, group1) { false }
@@ -1016,21 +992,21 @@ RSpec.describe API::Groups, feature_category: :subgroups do
group3.add_owner(user3)
end
- it 'does not change visibility when not requested' do
+ it 'does not change visibility when not requested', :aggregate_failures do
put api("/groups/#{group3.id}", user3), params: { description: 'Bug #23083' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['visibility']).to eq('public')
end
- it 'prevents making private a group containing public subgroups' do
+ it 'prevents making private a group containing public subgroups', :aggregate_failures do
put api("/groups/#{group3.id}", user3), params: { visibility: 'private' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['visibility_level']).to contain_exactly('private is not allowed since there are sub-groups with higher visibility.')
end
- it 'does not update prevent_sharing_groups_outside_hierarchy' do
+ it 'does not update prevent_sharing_groups_outside_hierarchy', :aggregate_failures do
put api("/groups/#{subgroup.id}", user3), params: { description: 'it works', prevent_sharing_groups_outside_hierarchy: true }
expect(response).to have_gitlab_http_status(:ok)
@@ -1042,17 +1018,17 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'when authenticated as the admin' do
- it 'updates the group' do
- put api("/groups/#{group1.id}", admin), params: { name: new_group_name }
+ it 'updates the group', :aggregate_failures do
+ put api("/groups/#{group1.id}", admin, admin_mode: true), params: { name: new_group_name }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(new_group_name)
end
- it 'ignores visibility level restrictions' do
+ it 'ignores visibility level restrictions', :aggregate_failures do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
- put api("/groups/#{group1.id}", admin), params: { visibility: 'internal' }
+ put api("/groups/#{group1.id}", admin, admin_mode: true), params: { visibility: 'internal' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['visibility']).to eq('internal')
@@ -1094,7 +1070,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
end
- it "returns the group's projects" do
+ it "returns the group's projects", :aggregate_failures do
get api("/groups/#{group1.id}/projects", user1)
expect(response).to have_gitlab_http_status(:ok)
@@ -1106,7 +1082,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'and using archived' do
- it "returns the group's archived projects" do
+ it "returns the group's archived projects", :aggregate_failures do
get api("/groups/#{group1.id}/projects?archived=true", user1)
expect(response).to have_gitlab_http_status(:ok)
@@ -1116,7 +1092,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.map { |project| project['id'] }).to include(archived_project.id)
end
- it "returns the group's non-archived projects" do
+ it "returns the group's non-archived projects", :aggregate_failures do
get api("/groups/#{group1.id}/projects?archived=false", user1)
expect(response).to have_gitlab_http_status(:ok)
@@ -1126,7 +1102,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.map { |project| project['id'] }).not_to include(archived_project.id)
end
- it "returns all of the group's projects" do
+ it "returns all of the group's projects", :aggregate_failures do
get api("/groups/#{group1.id}/projects", user1)
expect(response).to have_gitlab_http_status(:ok)
@@ -1150,7 +1126,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
group_with_projects.add_owner(user1)
end
- it 'returns items based ordered by similarity' do
+ it 'returns items based ordered by similarity', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -1166,7 +1142,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
params.delete(:search)
end
- it 'returns items ordered by name' do
+ it 'returns items ordered by name', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -1179,7 +1155,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
end
- it "returns the group's projects with simple representation" do
+ it "returns the group's projects with simple representation", :aggregate_failures do
get api("/groups/#{group1.id}/projects", user1), params: { simple: true }
expect(response).to have_gitlab_http_status(:ok)
@@ -1190,7 +1166,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.first['visibility']).not_to be_present
end
- it "filters the groups projects" do
+ it "filters the groups projects", :aggregate_failures do
public_project = create(:project, :public, path: 'test1', group: group1)
get api("/groups/#{group1.id}/projects", user1), params: { visibility: 'public' }
@@ -1202,7 +1178,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.first['name']).to eq(public_project.name)
end
- it "returns projects excluding shared" do
+ it "returns projects excluding shared", :aggregate_failures do
create(:project_group_link, project: create(:project), group: group1)
create(:project_group_link, project: create(:project), group: group1)
create(:project_group_link, project: create(:project), group: group1)
@@ -1227,7 +1203,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
group1.reload
end
- it "returns projects including those in subgroups" do
+ it "returns projects including those in subgroups", :aggregate_failures do
get api("/groups/#{group1.id}/projects", user1), params: { include_subgroups: true }
expect(response).to have_gitlab_http_status(:ok)
@@ -1236,7 +1212,10 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.length).to eq(6)
end
- it 'avoids N+1 queries', :use_sql_query_cache, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/383788' do
+ it 'avoids N+1 queries', :aggregate_failures, :use_sql_query_cache, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/383788' do
+ get api("/groups/#{group1.id}/projects", user1), params: { include_subgroups: true }
+ expect(respone).to have_gitlab_http_status(:ok)
+
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
get api("/groups/#{group1.id}/projects", user1), params: { include_subgroups: true }
end
@@ -1250,7 +1229,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'when include_ancestor_groups is true' do
- it 'returns ancestors groups projects' do
+ it 'returns ancestors groups projects', :aggregate_failures do
subgroup = create(:group, parent: group1)
subgroup_project = create(:project, group: subgroup)
@@ -1275,7 +1254,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(response).to have_gitlab_http_status(:not_found)
end
- it "only returns projects to which user has access" do
+ it "only returns projects to which user has access", :aggregate_failures do
project3.add_developer(user3)
get api("/groups/#{group1.id}/projects", user3)
@@ -1286,7 +1265,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.first['name']).to eq(project3.name)
end
- it 'only returns the projects owned by user' do
+ it 'only returns the projects owned by user', :aggregate_failures do
project2.group.add_owner(user3)
get api("/groups/#{project2.group.id}/projects", user3), params: { owned: true }
@@ -1296,7 +1275,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.first['name']).to eq(project2.name)
end
- it 'only returns the projects starred by user' do
+ it 'only returns the projects starred by user', :aggregate_failures do
user1.starred_projects = [project1]
get api("/groups/#{group1.id}/projects", user1), params: { starred: true }
@@ -1306,8 +1285,9 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.first['name']).to eq(project1.name)
end
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', :aggregate_failures do
get api("/groups/#{group1.id}/projects", user1)
+ expect(response).to have_gitlab_http_status(:ok)
control_count = ActiveRecord::QueryRecorder.new do
get api("/groups/#{group1.id}/projects", user1)
@@ -1322,8 +1302,8 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context "when authenticated as admin" do
- it "returns any existing group" do
- get api("/groups/#{group2.id}/projects", admin)
+ it "returns any existing group", :aggregate_failures do
+ get api("/groups/#{group2.id}/projects", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -1332,15 +1312,15 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
it "does not return a non existing group" do
- get api("/groups/#{non_existing_record_id}/projects", admin)
+ get api("/groups/#{non_existing_record_id}/projects", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when using group path in URL' do
- it 'returns any existing group' do
- get api("/groups/#{group1.path}/projects", admin)
+ it 'returns any existing group', :aggregate_failures do
+ get api("/groups/#{group1.path}/projects", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -1349,7 +1329,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
it 'does not return a non existing group' do
- get api('/groups/unknown/projects', admin)
+ get api('/groups/unknown/projects', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -1375,7 +1355,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'when authenticated as user' do
- it 'returns the shared projects in the group' do
+ it 'returns the shared projects in the group', :aggregate_failures do
get api(path, user1)
expect(response).to have_gitlab_http_status(:ok)
@@ -1386,7 +1366,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.first['visibility']).to be_present
end
- it 'returns shared projects with min access level or higher' do
+ it 'returns shared projects with min access level or higher', :aggregate_failures do
user = create(:user)
project2.add_guest(user)
@@ -1399,7 +1379,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.first['id']).to eq(project4.id)
end
- it 'returns the shared projects of the group with simple representation' do
+ it 'returns the shared projects of the group with simple representation', :aggregate_failures do
get api(path, user1), params: { simple: true }
expect(response).to have_gitlab_http_status(:ok)
@@ -1410,7 +1390,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.first['visibility']).not_to be_present
end
- it 'filters the shared projects in the group based on visibility' do
+ it 'filters the shared projects in the group based on visibility', :aggregate_failures do
internal_project = create(:project, :internal, namespace: create(:group))
create(:project_group_link, project: internal_project, group: group1)
@@ -1424,7 +1404,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.first['id']).to eq(internal_project.id)
end
- it 'filters the shared projects in the group based on search params' do
+ it 'filters the shared projects in the group based on search params', :aggregate_failures do
get api(path, user1), params: { search: 'test_project' }
expect(response).to have_gitlab_http_status(:ok)
@@ -1434,7 +1414,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.first['id']).to eq(project4.id)
end
- it 'does not return the projects owned by the group' do
+ it 'does not return the projects owned by the group', :aggregate_failures do
get api(path, user1)
expect(response).to have_gitlab_http_status(:ok)
@@ -1459,7 +1439,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(response).to have_gitlab_http_status(:not_found)
end
- it 'only returns shared projects to which user has access' do
+ it 'only returns shared projects to which user has access', :aggregate_failures do
project4.add_developer(user3)
get api(path, user3)
@@ -1470,7 +1450,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response.first['id']).to eq(project4.id)
end
- it 'only returns the projects starred by user' do
+ it 'only returns the projects starred by user', :aggregate_failures do
user1.starred_projects = [project2]
get api(path, user1), params: { starred: true }
@@ -1482,9 +1462,9 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context "when authenticated as admin" do
- subject { get api(path, admin) }
+ subject { get api(path, admin, admin_mode: true) }
- it "returns shared projects of an existing group" do
+ it "returns shared projects of an existing group", :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -1504,7 +1484,10 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
end
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', :aggregate_failures, :use_sql_query_cache do
+ subject
+ expect(response).to have_gitlab_http_status(:ok)
+
control_count = ActiveRecord::QueryRecorder.new do
subject
end.count
@@ -1520,8 +1503,8 @@ RSpec.describe API::Groups, feature_category: :subgroups do
context 'when using group path in URL' do
let(:path) { "/groups/#{group1.path}/projects/shared" }
- it 'returns the right details' do
- get api(path, admin)
+ it 'returns the right details', :aggregate_failures do
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -1531,7 +1514,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
it 'returns 404 for a non-existent group' do
- get api('/groups/unknown/projects/shared', admin)
+ get api('/groups/unknown/projects/shared', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -1544,7 +1527,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
let!(:subgroup3) { create(:group, :private, parent: group2) }
context 'when unauthenticated' do
- it 'returns only public subgroups' do
+ it 'returns only public subgroups', :aggregate_failures do
get api("/groups/#{group1.id}/subgroups")
expect(response).to have_gitlab_http_status(:ok)
@@ -1562,7 +1545,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'when statistics are requested' do
- it 'does not include statistics' do
+ it 'does not include statistics', :aggregate_failures do
get api("/groups/#{group1.id}/subgroups"), params: { statistics: true }
expect(response).to have_gitlab_http_status(:ok)
@@ -1575,7 +1558,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
context 'when authenticated as user' do
context 'when user is not member of a public group' do
- it 'returns no subgroups for the public group' do
+ it 'returns no subgroups for the public group', :aggregate_failures do
get api("/groups/#{group1.id}/subgroups", user2)
expect(response).to have_gitlab_http_status(:ok)
@@ -1584,7 +1567,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'when using all_available in request' do
- it 'returns public subgroups' do
+ it 'returns public subgroups', :aggregate_failures do
get api("/groups/#{group1.id}/subgroups", user2), params: { all_available: true }
expect(response).to have_gitlab_http_status(:ok)
@@ -1609,7 +1592,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
group1.add_guest(user2)
end
- it 'returns private subgroups' do
+ it 'returns private subgroups', :aggregate_failures do
get api("/groups/#{group1.id}/subgroups", user2)
expect(response).to have_gitlab_http_status(:ok)
@@ -1623,7 +1606,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'when using statistics in request' do
- it 'does not include statistics' do
+ it 'does not include statistics', :aggregate_failures do
get api("/groups/#{group1.id}/subgroups", user2), params: { statistics: true }
expect(response).to have_gitlab_http_status(:ok)
@@ -1638,7 +1621,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
group2.add_guest(user1)
end
- it 'returns subgroups' do
+ it 'returns subgroups', :aggregate_failures do
get api("/groups/#{group2.id}/subgroups", user1)
expect(response).to have_gitlab_http_status(:ok)
@@ -1651,32 +1634,32 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'when authenticated as admin' do
- it 'returns private subgroups of a public group' do
- get api("/groups/#{group1.id}/subgroups", admin)
+ it 'returns private subgroups of a public group', :aggregate_failures do
+ get api("/groups/#{group1.id}/subgroups", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
- it 'returns subgroups of a private group' do
- get api("/groups/#{group2.id}/subgroups", admin)
+ it 'returns subgroups of a private group', :aggregate_failures do
+ get api("/groups/#{group2.id}/subgroups", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
end
- it 'does not include statistics by default' do
- get api("/groups/#{group1.id}/subgroups", admin)
+ it 'does not include statistics by default', :aggregate_failures do
+ get api("/groups/#{group1.id}/subgroups", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.first).not_to include('statistics')
end
- it 'includes statistics if requested' do
- get api("/groups/#{group1.id}/subgroups", admin), params: { statistics: true }
+ it 'includes statistics if requested', :aggregate_failures do
+ get api("/groups/#{group1.id}/subgroups", admin, admin_mode: true), params: { statistics: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
@@ -1700,7 +1683,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
let(:response_groups) { json_response.map { |group| group['name'] } }
context 'when unauthenticated' do
- it 'returns only public descendants' do
+ it 'returns only public descendants', :aggregate_failures do
get api("/groups/#{group1.id}/descendant_groups")
expect(response).to have_gitlab_http_status(:ok)
@@ -1719,7 +1702,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
context 'when authenticated as user' do
context 'when user is not member of a public group' do
- it 'returns no descendants for the public group' do
+ it 'returns no descendants for the public group', :aggregate_failures do
get api("/groups/#{group1.id}/descendant_groups", user2)
expect(response).to have_gitlab_http_status(:ok)
@@ -1728,7 +1711,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'when using all_available in request' do
- it 'returns public descendants' do
+ it 'returns public descendants', :aggregate_failures do
get api("/groups/#{group1.id}/descendant_groups", user2), params: { all_available: true }
expect(response).to have_gitlab_http_status(:ok)
@@ -1752,7 +1735,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
group1.add_guest(user2)
end
- it 'returns private descendants' do
+ it 'returns private descendants', :aggregate_failures do
get api("/groups/#{group1.id}/descendant_groups", user2)
expect(response).to have_gitlab_http_status(:ok)
@@ -1763,7 +1746,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'when using statistics in request' do
- it 'does not include statistics' do
+ it 'does not include statistics', :aggregate_failures do
get api("/groups/#{group1.id}/descendant_groups", user2), params: { statistics: true }
expect(response).to have_gitlab_http_status(:ok)
@@ -1778,7 +1761,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
group2.add_guest(user1)
end
- it 'returns descendants' do
+ it 'returns descendants', :aggregate_failures do
get api("/groups/#{group2.id}/descendant_groups", user1)
expect(response).to have_gitlab_http_status(:ok)
@@ -1790,32 +1773,32 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'when authenticated as admin' do
- it 'returns private descendants of a public group' do
- get api("/groups/#{group1.id}/descendant_groups", admin)
+ it 'returns private descendants of a public group', :aggregate_failures do
+ get api("/groups/#{group1.id}/descendant_groups", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
end
- it 'returns descendants of a private group' do
- get api("/groups/#{group2.id}/descendant_groups", admin)
+ it 'returns descendants of a private group', :aggregate_failures do
+ get api("/groups/#{group2.id}/descendant_groups", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
- it 'does not include statistics by default' do
- get api("/groups/#{group1.id}/descendant_groups", admin)
+ it 'does not include statistics by default', :aggregate_failures do
+ get api("/groups/#{group1.id}/descendant_groups", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response.first).not_to include('statistics')
end
- it 'includes statistics if requested' do
- get api("/groups/#{group1.id}/descendant_groups", admin), params: { statistics: true }
+ it 'includes statistics if requested', :aggregate_failures do
+ get api("/groups/#{group1.id}/descendant_groups", admin, admin_mode: true), params: { statistics: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
@@ -1880,7 +1863,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context "when authenticated as user with group permissions" do
- it "creates group" do
+ it "creates group", :aggregate_failures do
group = attributes_for_group_api request_access_enabled: false
post api("/groups", user3), params: group
@@ -1893,7 +1876,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(json_response["visibility"]).to eq(Gitlab::VisibilityLevel.string_level(Gitlab::CurrentSettings.current_application_settings.default_group_visibility))
end
- it "creates a nested group" do
+ it "creates a nested group", :aggregate_failures do
parent = create(:group)
parent.add_owner(user3)
group = attributes_for_group_api parent_id: parent.id
@@ -1926,7 +1909,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
subject { post api("/groups", user3), params: params }
context 'for users who have the ability to create a group with `default_branch_protection`' do
- it 'creates group with the specified branch protection level' do
+ it 'creates group with the specified branch protection level', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:created)
@@ -1935,7 +1918,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'for users who do not have the ability to create a group with `default_branch_protection`' do
- it 'does not create the group with the specified branch protection level' do
+ it 'does not create the group with the specified branch protection level', :aggregate_failures do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(user3, :create_group_with_default_branch_protection) { false }
@@ -1947,7 +1930,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
end
- it "does not create group, duplicate" do
+ it "does not create group, duplicate", :aggregate_failures do
post api("/groups", user3), params: { name: 'Duplicate Test', path: group2.path }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -2007,13 +1990,13 @@ RSpec.describe API::Groups, feature_category: :subgroups do
context "when authenticated as admin" do
it "removes any existing group" do
- delete api("/groups/#{group2.id}", admin)
+ delete api("/groups/#{group2.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:accepted)
end
it "does not remove a non existing group" do
- delete api("/groups/#{non_existing_record_id}", admin)
+ delete api("/groups/#{non_existing_record_id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -2040,7 +2023,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
context "when authenticated as admin" do
it "transfers project to group" do
- post api("/groups/#{group1.id}/projects/#{project.id}", admin)
+ post api("/groups/#{group1.id}/projects/#{project.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:created)
end
@@ -2048,7 +2031,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
context 'when using project path in URL' do
context 'with a valid project path' do
it "transfers project to group" do
- post api("/groups/#{group1.id}/projects/#{project_path}", admin)
+ post api("/groups/#{group1.id}/projects/#{project_path}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:created)
end
@@ -2056,7 +2039,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
context 'with a non-existent project path' do
it "does not transfer project to group" do
- post api("/groups/#{group1.id}/projects/nogroup%2Fnoproject", admin)
+ post api("/groups/#{group1.id}/projects/nogroup%2Fnoproject", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -2066,7 +2049,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
context 'when using a group path in URL' do
context 'with a valid group path' do
it "transfers project to group" do
- post api("/groups/#{group1.path}/projects/#{project_path}", admin)
+ post api("/groups/#{group1.path}/projects/#{project_path}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:created)
end
@@ -2074,7 +2057,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
context 'with a non-existent group path' do
it "does not transfer project to group" do
- post api("/groups/noexist/projects/#{project_path}", admin)
+ post api("/groups/noexist/projects/#{project_path}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -2183,7 +2166,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
context 'when promoting a subgroup to a root group' do
shared_examples_for 'promotes the subgroup to a root group' do
- it 'returns success' do
+ it 'returns success', :aggregate_failures do
make_request(user)
expect(response).to have_gitlab_http_status(:created)
@@ -2207,7 +2190,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
let(:group) { create(:group) }
let(:params) { { group_id: '' } }
- it 'returns error' do
+ it 'returns error', :aggregate_failures do
make_request(user)
expect(response).to have_gitlab_http_status(:bad_request)
@@ -2258,7 +2241,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
end
- it 'returns error' do
+ it 'returns error', :aggregate_failures do
make_request(user)
expect(response).to have_gitlab_http_status(:bad_request)
@@ -2267,7 +2250,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
end
context 'when the transfer succceds' do
- it 'returns success' do
+ it 'returns success', :aggregate_failures do
make_request(user)
expect(response).to have_gitlab_http_status(:created)
@@ -2289,11 +2272,13 @@ RSpec.describe API::Groups, feature_category: :subgroups do
describe "POST /groups/:id/share" do
shared_examples 'shares group with group' do
- it "shares group with group" do
+ let_it_be(:admin_mode) { false }
+
+ it "shares group with group", :aggregate_failures do
expires_at = 10.days.from_now.to_date
expect do
- post api("/groups/#{group.id}/share", user), params: { group_id: shared_with_group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at }
+ post api("/groups/#{group.id}/share", user, admin_mode: admin_mode), params: { group_id: shared_with_group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at }
end.to change { group.shared_with_group_links.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -2322,7 +2307,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
expect(response).to have_gitlab_http_status(:not_found)
end
- it "returns a 400 error when wrong params passed" do
+ it "returns a 400 error when wrong params passed", :aggregate_failures do
post api("/groups/#{group.id}/share", user), params: { group_id: shared_with_group.id, group_access: non_existing_record_access_level }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -2375,15 +2360,18 @@ RSpec.describe API::Groups, feature_category: :subgroups do
let(:user) { admin }
let(:group) { create(:group) }
let(:shared_with_group) { create(:group) }
+ let(:admin_mode) { true }
end
end
end
describe 'DELETE /groups/:id/share/:group_id' do
shared_examples 'deletes group share' do
- it 'deletes a group share' do
+ let_it_be(:admin_mode) { false }
+
+ it 'deletes a group share', :aggregate_failures do
expect do
- delete api("/groups/#{shared_group.id}/share/#{shared_with_group.id}", user)
+ delete api("/groups/#{shared_group.id}/share/#{shared_with_group.id}", user, admin_mode: admin_mode)
expect(response).to have_gitlab_http_status(:no_content)
expect(shared_group.shared_with_group_links).to be_empty
@@ -2432,7 +2420,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
create(:group_group_link, shared_group: group1, shared_with_group: group_a)
end
- it 'does not remove group share' do
+ it 'does not remove group share', :aggregate_failures do
expect do
delete api("/groups/#{group1.id}/share/#{group_a.id}", user4)
@@ -2452,6 +2440,7 @@ RSpec.describe API::Groups, feature_category: :subgroups do
let(:user) { admin }
let(:shared_group) { group2 }
let(:shared_with_group) { group_b }
+ let(:admin_mode) { true }
end
end
end
diff --git a/spec/requests/api/helm_packages_spec.rb b/spec/requests/api/helm_packages_spec.rb
index 584f6e3c7d4..d6afd6f86ff 100644
--- a/spec/requests/api/helm_packages_spec.rb
+++ b/spec/requests/api/helm_packages_spec.rb
@@ -17,7 +17,15 @@ RSpec.describe API::HelmPackages, feature_category: :package_registry do
let_it_be(:package_file2_2) { create(:helm_package_file, package: package2, file_sha256: 'file2', file_name: 'filename2.tgz', channel: 'test', description: 'hello from test channel') }
let_it_be(:other_package) { create(:npm_package, project: project) }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_helm_user' } }
+ let(:snowplow_gitlab_standard_context) { snowplow_context }
+
+ def snowplow_context(user_role: :developer)
+ if user_role == :anonymous
+ { project: project, namespace: project.namespace, property: 'i_package_helm_user' }
+ else
+ { project: project, namespace: project.namespace, property: 'i_package_helm_user', user: user }
+ end
+ end
describe 'GET /api/v4/projects/:id/packages/helm/:channel/index.yaml' do
let(:project_id) { project.id }
@@ -65,6 +73,7 @@ RSpec.describe API::HelmPackages, feature_category: :package_registry do
with_them do
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, personal_access_token.token) }
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
before do
project.update!(visibility: visibility.to_s)
@@ -75,6 +84,8 @@ RSpec.describe API::HelmPackages, feature_category: :package_registry do
end
context 'with access to package registry for everyone' do
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: :anonymous) }
+
before do
project.update!(visibility: Gitlab::VisibilityLevel::PRIVATE)
project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
@@ -116,6 +127,7 @@ RSpec.describe API::HelmPackages, feature_category: :package_registry do
with_them do
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, personal_access_token.token) }
let(:headers) { user_headers.merge(workhorse_headers) }
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
@@ -178,6 +190,7 @@ RSpec.describe API::HelmPackages, feature_category: :package_registry do
with_them do
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, personal_access_token.token) }
let(:headers) { user_headers.merge(workhorse_headers) }
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 38275ce0057..0be9df41e8f 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require 'raven/transports/dummy'
require_relative '../../../config/initializers/sentry'
-RSpec.describe API::Helpers, :enable_admin_mode, feature_category: :authentication_and_authorization do
+RSpec.describe API::Helpers, :enable_admin_mode, feature_category: :system_access do
include API::APIGuard::HelperMethods
include described_class
include TermsHelper
diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb
index 0d75bb94144..9b5ae72526c 100644
--- a/spec/requests/api/import_github_spec.rb
+++ b/spec/requests/api/import_github_spec.rb
@@ -174,72 +174,54 @@ RSpec.describe API::ImportGithub, feature_category: :importers do
let_it_be(:user) { create(:user) }
let(:params) { { personal_access_token: token } }
- context 'when feature github_import_gists is enabled' do
+ context 'when gists import was started' do
before do
- stub_feature_flags(github_import_gists: true)
+ allow(Import::Github::GistsImportService)
+ .to receive(:new).with(user, client, access_params)
+ .and_return(double(execute: { status: :success }))
end
- context 'when gists import was started' do
- before do
- allow(Import::Github::GistsImportService)
- .to receive(:new).with(user, client, access_params)
- .and_return(double(execute: { status: :success }))
- end
-
- it 'returns 202' do
- post api('/import/github/gists', user), params: params
+ it 'returns 202' do
+ post api('/import/github/gists', user), params: params
- expect(response).to have_gitlab_http_status(:accepted)
- end
+ expect(response).to have_gitlab_http_status(:accepted)
end
+ end
- context 'when gists import is in progress' do
- before do
- allow(Import::Github::GistsImportService)
- .to receive(:new).with(user, client, access_params)
- .and_return(double(execute: { status: :error, message: 'Import already in progress', http_status: :unprocessable_entity }))
- end
-
- it 'returns 422 error' do
- post api('/import/github/gists', user), params: params
-
- expect(response).to have_gitlab_http_status(:unprocessable_entity)
- expect(json_response['errors']).to eq('Import already in progress')
- end
+ context 'when gists import is in progress' do
+ before do
+ allow(Import::Github::GistsImportService)
+ .to receive(:new).with(user, client, access_params)
+ .and_return(double(execute: { status: :error, message: 'Import already in progress', http_status: :unprocessable_entity }))
end
- context 'when unauthenticated user' do
- it 'returns 403 error' do
- post api('/import/github/gists'), params: params
+ it 'returns 422 error' do
+ post api('/import/github/gists', user), params: params
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response['errors']).to eq('Import already in progress')
end
+ end
- context 'when rate limit reached' do
- before do
- allow(Import::Github::GistsImportService)
- .to receive(:new).with(user, client, access_params)
- .and_raise(Gitlab::GithubImport::RateLimitError)
- end
-
- it 'returns 429 error' do
- post api('/import/github/gists', user), params: params
+ context 'when unauthenticated user' do
+ it 'returns 403 error' do
+ post api('/import/github/gists'), params: params
- expect(response).to have_gitlab_http_status(:too_many_requests)
- end
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
end
- context 'when feature github_import_gists is disabled' do
+ context 'when rate limit reached' do
before do
- stub_feature_flags(github_import_gists: false)
+ allow(Import::Github::GistsImportService)
+ .to receive(:new).with(user, client, access_params)
+ .and_raise(Gitlab::GithubImport::RateLimitError)
end
- it 'returns 404 error' do
+ it 'returns 429 error' do
post api('/import/github/gists', user), params: params
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:too_many_requests)
end
end
end
diff --git a/spec/requests/api/integrations/slack/events_spec.rb b/spec/requests/api/integrations/slack/events_spec.rb
new file mode 100644
index 00000000000..438715db4f0
--- /dev/null
+++ b/spec/requests/api/integrations/slack/events_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Integrations::Slack::Events, feature_category: :integrations do
+ describe 'POST /integrations/slack/events' do
+ let_it_be(:slack_installation) { create(:slack_integration) }
+
+ let(:params) { {} }
+ let(:headers) do
+ {
+ ::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER => Time.current.to_i.to_s,
+ ::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER => 'mock_verified_signature'
+ }
+ end
+
+ before do
+ allow(ActiveSupport::SecurityUtils).to receive(:secure_compare) do |signature|
+ signature == 'mock_verified_signature'
+ end
+
+ stub_application_setting(slack_app_signing_secret: 'mock_key')
+ end
+
+ subject { post api('/integrations/slack/events'), params: params, headers: headers }
+
+ it_behaves_like 'Slack request verification'
+
+ context 'when type param is unknown' do
+ let(:params) do
+ { type: 'unknown_type' }
+ end
+
+ it 'generates a tracked error' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).once
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(response.body).to be_empty
+ end
+ end
+
+ context 'when type param is url_verification' do
+ let(:params) do
+ {
+ type: 'url_verification',
+ challenge: '3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P'
+ }
+ end
+
+ it 'responds in-request with the challenge' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq({ 'challenge' => '3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P' })
+ end
+ end
+
+ context 'when event.type param is app_home_opened' do
+ let(:params) do
+ {
+ type: 'event_callback',
+ team_id: slack_installation.team_id,
+ event_id: 'Ev03SA75UJKB',
+ event: {
+ type: 'app_home_opened',
+ user: 'U0123ABCDEF'
+ }
+ }
+ end
+
+ it 'calls the Slack API (integration-style test)', :sidekiq_inline, :clean_gitlab_redis_shared_state do
+ api_url = "#{Slack::API::BASE_URL}/views.publish"
+
+ stub_request(:post, api_url)
+ .to_return(
+ status: 200,
+ body: { ok: true }.to_json,
+ headers: { 'Content-Type' => 'application/json' }
+ )
+
+ subject
+
+ expect(WebMock).to have_requested(:post, api_url)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq('{}')
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/integrations/slack/interactions_spec.rb b/spec/requests/api/integrations/slack/interactions_spec.rb
new file mode 100644
index 00000000000..35a96be75e0
--- /dev/null
+++ b/spec/requests/api/integrations/slack/interactions_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Integrations::Slack::Interactions, feature_category: :integrations do
+ describe 'POST /integrations/slack/interactions' do
+ let_it_be(:slack_installation) { create(:slack_integration) }
+
+ let(:payload) { {} }
+ let(:params) { { payload: Gitlab::Json.dump(payload) } }
+
+ let(:headers) do
+ {
+ ::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER => Time.current.to_i.to_s,
+ ::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER => 'mock_verified_signature'
+ }
+ end
+
+ before do
+ allow(ActiveSupport::SecurityUtils).to receive(:secure_compare) do |signature|
+ signature == 'mock_verified_signature'
+ end
+
+ stub_application_setting(slack_app_signing_secret: 'mock_key')
+ end
+
+ subject { post api('/integrations/slack/interactions'), params: params, headers: headers }
+
+ it_behaves_like 'Slack request verification'
+
+ context 'when type param is unknown' do
+ let(:payload) do
+ { type: 'unknown_type' }
+ end
+
+ it 'generates a tracked error' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).once
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(response.body).to be_empty
+ end
+ end
+
+ context 'when event.type param is view_closed' do
+ let(:payload) do
+ {
+ type: 'view_closed',
+ team_id: slack_installation.team_id,
+ event: {
+ type: 'view_closed',
+ user: 'U0123ABCDEF'
+ }
+ }
+ end
+
+ it 'calls the Slack Interactivity Service' do
+ expect_next_instance_of(::Integrations::SlackInteractionService) do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/integrations/slack/options_spec.rb b/spec/requests/api/integrations/slack/options_spec.rb
new file mode 100644
index 00000000000..eef993d0329
--- /dev/null
+++ b/spec/requests/api/integrations/slack/options_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Integrations::Slack::Options, feature_category: :integrations do
+ describe 'POST /integrations/slack/options' do
+ let_it_be(:slack_installation) { create(:slack_integration) }
+
+ let(:payload) { {} }
+ let(:params) { { payload: Gitlab::Json.dump(payload) } }
+
+ let(:headers) do
+ {
+ ::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER => Time.current.to_i.to_s,
+ ::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER => 'mock_verified_signature'
+ }
+ end
+
+ before do
+ allow(ActiveSupport::SecurityUtils).to receive(:secure_compare) do |signature|
+ signature == 'mock_verified_signature'
+ end
+
+ stub_application_setting(slack_app_signing_secret: 'mock_key')
+ end
+
+ subject(:post_to_slack_api) { post api('/integrations/slack/options'), params: params, headers: headers }
+
+ it_behaves_like 'Slack request verification'
+
+ context 'when type param is unknown' do
+ let(:payload) do
+ { action_id: 'unknown_action' }
+ end
+
+ it 'generates a tracked error' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).once
+
+ post_to_slack_api
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(response.body).to be_empty
+ end
+ end
+
+ context 'when action_id param is assignee' do
+ let(:payload) do
+ {
+ action_id: 'assignee'
+ }
+ end
+
+ it 'calls the Slack Interactivity Service' do
+ expect_next_instance_of(::Integrations::SlackOptionService) do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ post_to_slack_api
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/integrations_spec.rb b/spec/requests/api/integrations_spec.rb
index c35b9bab0ec..8d348dc0a54 100644
--- a/spec/requests/api/integrations_spec.rb
+++ b/spec/requests/api/integrations_spec.rb
@@ -10,14 +10,6 @@ RSpec.describe API::Integrations, feature_category: :integrations do
create(:project, creator_id: user.id, namespace: user.namespace)
end
- # The API supports all integrations except the GitLab Slack Application
- # integration; this integration must be installed via the UI.
- def self.integration_names
- names = Integration.available_integration_names
- names.delete(Integrations::GitlabSlackApplication.to_param) if Gitlab.ee?
- names
- end
-
%w[integrations services].each do |endpoint|
describe "GET /projects/:id/#{endpoint}" do
it 'returns authentication error when unauthenticated' do
@@ -51,9 +43,19 @@ RSpec.describe API::Integrations, feature_category: :integrations do
end
end
- integration_names.each do |integration|
+ where(:integration) do
+ # The API supports all integrations except the GitLab Slack Application
+ # integration; this integration must be installed via the UI.
+ names = Integration.available_integration_names
+ names.delete(Integrations::GitlabSlackApplication.to_param) if Gitlab.ee?
+ names - %w[shimo zentao]
+ end
+
+ with_them do
+ integration = params[:integration]
+
describe "PUT /projects/:id/#{endpoint}/#{integration.dasherize}" do
- include_context integration
+ include_context 'with integration'
# NOTE: Some attributes are not supported for PUT requests, even though they probably should be.
# We can fix these manually, or with a generic approach like https://gitlab.com/gitlab-org/gitlab/-/issues/348208
@@ -62,7 +64,7 @@ RSpec.describe API::Integrations, feature_category: :integrations do
datadog: %i[archive_trace_events],
discord: %i[branches_to_be_notified notify_only_broken_pipelines],
hangouts_chat: %i[notify_only_broken_pipelines],
- jira: %i[issues_enabled project_key vulnerabilities_enabled vulnerabilities_issuetype],
+ jira: %i[issues_enabled project_key jira_issue_regex jira_issue_prefix vulnerabilities_enabled vulnerabilities_issuetype],
mattermost: %i[deployment_channel labels_to_be_notified],
mock_ci: %i[enable_ssl_verification],
prometheus: %i[manual_configuration],
@@ -119,7 +121,7 @@ RSpec.describe API::Integrations, feature_category: :integrations do
end
describe "DELETE /projects/:id/#{endpoint}/#{integration.dasherize}" do
- include_context integration
+ include_context 'with integration'
before do
initialize_integration(integration)
@@ -135,7 +137,7 @@ RSpec.describe API::Integrations, feature_category: :integrations do
end
describe "GET /projects/:id/#{endpoint}/#{integration.dasherize}" do
- include_context integration
+ include_context 'with integration'
let!(:initialized_integration) { initialize_integration(integration, active: true) }
@@ -367,7 +369,7 @@ RSpec.describe API::Integrations, feature_category: :integrations do
describe 'Jira integration' do
let(:integration_name) { 'jira' }
let(:params) do
- { url: 'https://jira.example.com', username: 'username', password: 'password' }
+ { url: 'https://jira.example.com', username: 'username', password: 'password', jira_auth_type: 0 }
end
before do
@@ -426,4 +428,28 @@ RSpec.describe API::Integrations, feature_category: :integrations do
expect(response_keys).not_to include(*integration.secret_fields)
end
end
+
+ describe 'POST /slack/trigger' do
+ before_all do
+ create(:gitlab_slack_application_integration, project: project)
+ end
+
+ before do
+ stub_application_setting(slack_app_verification_token: 'token')
+ end
+
+ it 'returns status 200' do
+ post api('/slack/trigger'), params: { token: 'token', text: 'help' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['response_type']).to eq("ephemeral")
+ end
+
+ it 'returns status 404 when token is invalid' do
+ post api('/slack/trigger'), params: { token: 'invalid', text: 'foo' }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['response_type']).to be_blank
+ end
+ end
end
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index ca32271f573..6414b1efe6a 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Internal::Base, feature_category: :authentication_and_authorization do
+RSpec.describe API::Internal::Base, feature_category: :system_access do
include GitlabShellHelpers
include APIInternalBaseHelpers
@@ -10,6 +10,9 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author
let_it_be(:project, reload: true) { create(:project, :repository, :wiki_repo) }
let_it_be(:personal_snippet) { create(:personal_snippet, :repository, author: user) }
let_it_be(:project_snippet) { create(:project_snippet, :repository, author: user, project: project) }
+ let_it_be(:max_pat_access_token_lifetime) do
+ PersonalAccessToken::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS.days.from_now.to_date.freeze
+ end
let(:key) { create(:key, user: user) }
let(:secret_token) { Gitlab::Shell.secret_token }
@@ -194,39 +197,68 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author
expect(json_response['message']).to match(/\AInvalid scope: 'badscope'. Valid scopes are: /)
end
- it 'returns a token without expiry when the expires_at parameter is missing' do
- token_size = (PersonalAccessToken.token_prefix || '').size + 20
+ it 'returns a token with expiry when it receives a valid expires_at parameter' do
+ freeze_time do
+ token_size = (PersonalAccessToken.token_prefix || '').size + 20
+
+ post api('/internal/personal_access_token'),
+ params: {
+ key_id: key.id,
+ name: 'newtoken',
+ scopes: %w(read_api read_repository),
+ expires_at: max_pat_access_token_lifetime
+ },
+ headers: gitlab_shell_internal_api_request_header
- post api('/internal/personal_access_token'),
- params: {
- key_id: key.id,
- name: 'newtoken',
- scopes: %w(read_api read_repository)
- },
- headers: gitlab_shell_internal_api_request_header
+ expect(json_response['success']).to be_truthy
+ expect(json_response['token']).to match(/\A\S{#{token_size}}\z/)
+ expect(json_response['scopes']).to match_array(%w(read_api read_repository))
+ expect(json_response['expires_at']).to eq(max_pat_access_token_lifetime.iso8601)
+ end
+ end
- expect(json_response['success']).to be_truthy
- expect(json_response['token']).to match(/\A\S{#{token_size}}\z/)
- expect(json_response['scopes']).to match_array(%w(read_api read_repository))
- expect(json_response['expires_at']).to be_nil
+ context 'when default_pat_expiration feature flag is true' do
+ it 'returns token with expiry as PersonalAccessToken::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS' do
+ freeze_time do
+ token_size = (PersonalAccessToken.token_prefix || '').size + 20
+
+ post api('/internal/personal_access_token'),
+ params: {
+ key_id: key.id,
+ name: 'newtoken',
+ scopes: %w(read_api read_repository)
+ },
+ headers: gitlab_shell_internal_api_request_header
+
+ expect(json_response['success']).to be_truthy
+ expect(json_response['token']).to match(/\A\S{#{token_size}}\z/)
+ expect(json_response['scopes']).to match_array(%w(read_api read_repository))
+ expect(json_response['expires_at']).to eq(max_pat_access_token_lifetime.iso8601)
+ end
+ end
end
- it 'returns a token with expiry when it receives a valid expires_at parameter' do
- token_size = (PersonalAccessToken.token_prefix || '').size + 20
+ context 'when default_pat_expiration feature flag is false' do
+ before do
+ stub_feature_flags(default_pat_expiration: false)
+ end
- post api('/internal/personal_access_token'),
- params: {
- key_id: key.id,
- name: 'newtoken',
- scopes: %w(read_api read_repository),
- expires_at: '9001-11-17'
- },
- headers: gitlab_shell_internal_api_request_header
+ it 'uses nil expiration value' do
+ token_size = (PersonalAccessToken.token_prefix || '').size + 20
+
+ post api('/internal/personal_access_token'),
+ params: {
+ key_id: key.id,
+ name: 'newtoken',
+ scopes: %w(read_api read_repository)
+ },
+ headers: gitlab_shell_internal_api_request_header
- expect(json_response['success']).to be_truthy
- expect(json_response['token']).to match(/\A\S{#{token_size}}\z/)
- expect(json_response['scopes']).to match_array(%w(read_api read_repository))
- expect(json_response['expires_at']).to eq('9001-11-17')
+ expect(json_response['success']).to be_truthy
+ expect(json_response['token']).to match(/\A\S{#{token_size}}\z/)
+ expect(json_response['scopes']).to match_array(%w(read_api read_repository))
+ expect(json_response['expires_at']).to be_nil
+ end
end
end
@@ -514,7 +546,7 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
expect(json_response["gl_key_type"]).to eq("key")
expect(json_response["gl_key_id"]).to eq(key.id)
- expect(user.reload.last_activity_on).to be_nil
+ expect(user.reload.last_activity_on).to eql(Date.today)
end
it_behaves_like 'sets hook env' do
@@ -553,7 +585,7 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author
expect(json_response["status"]).to be_truthy
expect(json_response["gl_project_path"]).to eq(personal_snippet.repository.full_path)
expect(json_response["gl_repository"]).to eq("snippet-#{personal_snippet.id}")
- expect(user.reload.last_activity_on).to be_nil
+ expect(user.reload.last_activity_on).to eql(Date.today)
end
it_behaves_like 'sets hook env' do
@@ -585,7 +617,7 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author
expect(json_response["status"]).to be_truthy
expect(json_response["gl_project_path"]).to eq(project_snippet.repository.full_path)
expect(json_response["gl_repository"]).to eq("snippet-#{project_snippet.id}")
- expect(user.reload.last_activity_on).to be_nil
+ expect(user.reload.last_activity_on).to eql(Date.today)
end
it_behaves_like 'sets hook env' do
@@ -703,7 +735,7 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author
expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path)
expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage))
expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
- expect(user.reload.last_activity_on).to be_nil
+ expect(user.reload.last_activity_on).to eql(Date.today)
end
it_behaves_like 'rate limited request' do
@@ -862,7 +894,7 @@ RSpec.describe API::Internal::Base, feature_category: :authentication_and_author
expect(json_response['status']).to be_truthy
expect(json_response['payload']).to eql(payload)
expect(json_response['gl_console_messages']).to eql(console_messages)
- expect(user.reload.last_activity_on).to be_nil
+ expect(user.reload.last_activity_on).to eql(Date.today)
end
end
end
diff --git a/spec/requests/api/internal/kubernetes_spec.rb b/spec/requests/api/internal/kubernetes_spec.rb
index be76e55269a..c07382a6e04 100644
--- a/spec/requests/api/internal/kubernetes_spec.rb
+++ b/spec/requests/api/internal/kubernetes_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Internal::Kubernetes, feature_category: :kubernetes_management do
+RSpec.describe API::Internal::Kubernetes, feature_category: :deployment_management do
let(:jwt_auth_headers) do
jwt_token = JWT.encode({ 'iss' => Gitlab::Kas::JWT_ISSUER }, Gitlab::Kas.secret, 'HS256')
@@ -59,12 +59,29 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :kubernetes_manageme
end
end
+ shared_examples 'error handling' do
+ let!(:agent_token) { create(:cluster_agent_token) }
+
+ # this test verifies fix for an issue where AgentToken passed in Authorization
+ # header broke error handling in the api_helpers.rb. It can be removed after
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/406582 is done
+ it 'returns correct error for the endpoint' do
+ allow(Gitlab::Kas).to receive(:verify_api_request).and_raise(StandardError.new('Unexpected Error'))
+
+ send_request(headers: { 'Authorization' => "Bearer #{agent_token.token}" })
+
+ expect(response).to have_gitlab_http_status(:internal_server_error)
+ expect(response.body).to include("Unexpected Error")
+ end
+ end
+
describe 'POST /internal/kubernetes/usage_metrics', :clean_gitlab_redis_shared_state do
def send_request(headers: {}, params: {})
post api('/internal/kubernetes/usage_metrics'), params: params, headers: headers.reverse_merge(jwt_auth_headers)
end
include_examples 'authorization'
+ include_examples 'error handling'
context 'is authenticated for an agent' do
let!(:agent_token) { create(:cluster_agent_token) }
@@ -147,19 +164,30 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :kubernetes_manageme
projects: [
{ id: project.full_path, default_namespace: 'staging' }
]
+ },
+ user_access: {
+ groups: [
+ { id: group.full_path }
+ ],
+ projects: [
+ { id: project.full_path }
+ ]
}
}
end
include_examples 'authorization'
+ include_examples 'error handling'
context 'agent exists' do
it 'configures the agent and returns a 204' do
send_request(params: { agent_id: agent.id, agent_config: config })
expect(response).to have_gitlab_http_status(:no_content)
- expect(agent.authorized_groups).to contain_exactly(group)
- expect(agent.authorized_projects).to contain_exactly(project)
+ expect(agent.ci_access_authorized_groups).to contain_exactly(group)
+ expect(agent.ci_access_authorized_projects).to contain_exactly(project)
+ expect(agent.user_access_authorized_groups).to contain_exactly(group)
+ expect(agent.user_access_authorized_projects).to contain_exactly(project)
end
end
@@ -179,6 +207,7 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :kubernetes_manageme
include_examples 'authorization'
include_examples 'agent authentication'
+ include_examples 'error handling'
context 'an agent is found' do
let!(:agent_token) { create(:cluster_agent_token) }
@@ -223,6 +252,7 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :kubernetes_manageme
include_examples 'authorization'
include_examples 'agent authentication'
+ include_examples 'error handling'
context 'an agent is found' do
let_it_be(:agent_token) { create(:cluster_agent_token) }
@@ -306,4 +336,145 @@ RSpec.describe API::Internal::Kubernetes, feature_category: :kubernetes_manageme
end
end
end
+
+ describe 'POST /internal/kubernetes/authorize_proxy_user', :clean_gitlab_redis_sessions do
+ include SessionHelpers
+
+ def send_request(headers: {}, params: {})
+ post api('/internal/kubernetes/authorize_proxy_user'), params: params, headers: headers.reverse_merge(jwt_auth_headers)
+ end
+
+ def stub_user_session(user, csrf_token)
+ stub_session(
+ {
+ 'warden.user.user.key' => [[user.id], user.authenticatable_salt],
+ '_csrf_token' => csrf_token
+ }
+ )
+ end
+
+ def stub_user_session_with_no_user_id(user, csrf_token)
+ stub_session(
+ {
+ 'warden.user.user.key' => [[nil], user.authenticatable_salt],
+ '_csrf_token' => csrf_token
+ }
+ )
+ end
+
+ def mask_token(encoded_token)
+ controller = ActionController::Base.new
+ raw_token = controller.send(:decode_csrf_token, encoded_token)
+ controller.send(:mask_token, raw_token)
+ end
+
+ def new_token
+ ActionController::Base.new.send(:generate_csrf_token)
+ end
+
+ let_it_be(:organization) { create(:group) }
+ let_it_be(:configuration_project) { create(:project, group: organization) }
+ let_it_be(:agent) { create(:cluster_agent, name: 'the-agent', project: configuration_project) }
+ let_it_be(:another_agent) { create(:cluster_agent) }
+ let_it_be(:deployment_project) { create(:project, group: organization) }
+ let_it_be(:deployment_group) { create(:group, parent: organization) }
+
+ let(:user_access_config) do
+ {
+ 'user_access' => {
+ 'access_as' => { 'agent' => {} },
+ 'projects' => [{ 'id' => deployment_project.full_path }],
+ 'groups' => [{ 'id' => deployment_group.full_path }]
+ }
+ }
+ end
+
+ let(:user) { create(:user) }
+
+ before do
+ allow(::Gitlab::Kas).to receive(:enabled?).and_return true
+ Clusters::Agents::Authorizations::UserAccess::RefreshService.new(agent, config: user_access_config).execute
+ end
+
+ it 'returns 400 when cookie is invalid' do
+ send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: '123', csrf_token: mask_token(new_token) })
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'returns 401 when session is not found' do
+ access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id('abc')
+ send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(new_token) })
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'returns 401 when CSRF token does not match' do
+ public_id = stub_user_session(user, new_token)
+ access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id)
+ send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(new_token) })
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'returns 404 for non-existent agent' do
+ token = new_token
+ public_id = stub_user_session(user, token)
+ access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id)
+ send_request(params: { agent_id: non_existing_record_id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) })
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'returns 403 when user has no access' do
+ token = new_token
+ public_id = stub_user_session(user, token)
+ access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id)
+ send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) })
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ it 'returns 200 when user has access' do
+ deployment_project.add_member(user, :developer)
+ token = new_token
+ public_id = stub_user_session(user, token)
+ access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id)
+ send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) })
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+
+ it 'returns 401 when user has valid KAS cookie and CSRF token but has no access to requested agent' do
+ deployment_project.add_member(user, :developer)
+ token = new_token
+ public_id = stub_user_session(user, token)
+ access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id)
+ send_request(params: { agent_id: another_agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) })
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ it 'returns 401 when global flag is disabled' do
+ stub_feature_flags(kas_user_access: false)
+
+ deployment_project.add_member(user, :developer)
+ token = new_token
+ public_id = stub_user_session(user, token)
+ access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id)
+ send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) })
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'returns 401 when user id is not found in session' do
+ deployment_project.add_member(user, :developer)
+ token = new_token
+ public_id = stub_user_session_with_no_user_id(user, token)
+ access_key = Gitlab::Kas::UserAccess.encrypt_public_session_id(public_id)
+ send_request(params: { agent_id: agent.id, access_type: 'session_cookie', access_key: access_key, csrf_token: mask_token(token) })
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
end
diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb
index 56f1089843b..1006319eabf 100644
--- a/spec/requests/api/internal/pages_spec.rb
+++ b/spec/requests/api/internal/pages_spec.rb
@@ -3,193 +3,97 @@
require 'spec_helper'
RSpec.describe API::Internal::Pages, feature_category: :pages do
- let(:auth_headers) do
- jwt_token = JWT.encode({ 'iss' => 'gitlab-pages' }, Gitlab::Pages.secret, 'HS256')
- { Gitlab::Pages::INTERNAL_API_REQUEST_HEADER => jwt_token }
+ let_it_be(:group) { create(:group) }
+ let_it_be_with_reload(:project) { create(:project, group: group) }
+
+ let(:auth_header) do
+ {
+ Gitlab::Pages::INTERNAL_API_REQUEST_HEADER => JWT.encode(
+ { 'iss' => 'gitlab-pages' },
+ Gitlab::Pages.secret, 'HS256')
+ }
end
- let(:pages_secret) { SecureRandom.random_bytes(Gitlab::Pages::SECRET_LENGTH) }
-
before do
- allow(Gitlab::Pages).to receive(:secret).and_return(pages_secret)
+ allow(Gitlab::Pages)
+ .to receive(:secret)
+ .and_return(SecureRandom.random_bytes(Gitlab::Pages::SECRET_LENGTH))
+
stub_pages_object_storage(::Pages::DeploymentUploader)
end
- describe "GET /internal/pages/status" do
- def query_enabled(headers = {})
- get api("/internal/pages/status"), headers: headers
- end
-
+ describe 'GET /internal/pages/status' do
it 'responds with 401 Unauthorized' do
- query_enabled
+ get api('/internal/pages/status')
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'responds with 204 no content' do
- query_enabled(auth_headers)
+ get api('/internal/pages/status'), headers: auth_header
expect(response).to have_gitlab_http_status(:no_content)
expect(response.body).to be_empty
end
end
- describe "GET /internal/pages" do
- def query_host(host, headers = {})
- get api("/internal/pages"), headers: headers, params: { host: host }
- end
-
- around do |example|
- freeze_time do
- example.run
- end
- end
-
- context 'not authenticated' do
+ describe 'GET /internal/pages' do
+ context 'when not authenticated' do
it 'responds with 401 Unauthorized' do
- query_host('pages.gitlab.io')
+ get api('/internal/pages')
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
- context 'authenticated' do
- def query_host(host)
- jwt_token = JWT.encode({ 'iss' => 'gitlab-pages' }, Gitlab::Pages.secret, 'HS256')
- headers = { Gitlab::Pages::INTERNAL_API_REQUEST_HEADER => jwt_token }
-
- super(host, headers)
+ context 'when authenticated' do
+ before do
+ project.update_pages_deployment!(create(:pages_deployment, project: project))
end
- def deploy_pages(project)
- deployment = create(:pages_deployment, project: project)
- project.mark_pages_as_deployed
- project.update_pages_deployment!(deployment)
+ around do |example|
+ freeze_time do
+ example.run
+ end
end
- context 'domain does not exist' do
+ context 'when domain does not exist' do
it 'responds with 204 no content' do
- query_host('pages.gitlab.io')
+ get api('/internal/pages'), headers: auth_header, params: { host: 'any-domain.gitlab.io' }
expect(response).to have_gitlab_http_status(:no_content)
expect(response.body).to be_empty
end
end
- context 'serverless domain' do
- let(:namespace) { create(:namespace, name: 'gitlab-org') }
- let(:project) { create(:project, namespace: namespace, name: 'gitlab-ce') }
- let(:environment) { create(:environment, project: project) }
- let(:pages_domain) { create(:pages_domain, domain: 'serverless.gitlab.io') }
- let(:knative_without_ingress) { create(:clusters_applications_knative) }
- let(:knative_with_ingress) { create(:clusters_applications_knative, external_ip: '10.0.0.1') }
-
- context 'without a knative ingress gateway IP' do
- let!(:serverless_domain_cluster) do
- create(
- :serverless_domain_cluster,
- uuid: 'abcdef12345678',
- pages_domain: pages_domain,
- knative: knative_without_ingress
- )
- end
-
- let(:serverless_domain) do
- create(
- :serverless_domain,
- serverless_domain_cluster: serverless_domain_cluster,
- environment: environment
- )
- end
-
- it 'responds with 204 no content' do
- query_host(serverless_domain.uri.host)
-
- expect(response).to have_gitlab_http_status(:no_content)
- expect(response.body).to be_empty
- end
- end
-
- context 'with a knative ingress gateway IP' do
- let!(:serverless_domain_cluster) do
- create(
- :serverless_domain_cluster,
- uuid: 'abcdef12345678',
- pages_domain: pages_domain,
- knative: knative_with_ingress
- )
- end
-
- let(:serverless_domain) do
- create(
- :serverless_domain,
- serverless_domain_cluster: serverless_domain_cluster,
- environment: environment
- )
- end
-
- it 'responds with 204 because of feature deprecation' do
- query_host(serverless_domain.uri.host)
+ context 'when querying a custom domain' do
+ let_it_be(:pages_domain) { create(:pages_domain, domain: 'pages.io', project: project) }
- expect(response).to have_gitlab_http_status(:no_content)
- expect(response.body).to be_empty
-
- ##
- # Serverless serving and reverse proxy to Kubernetes / Knative has
- # been deprecated and disabled, as per
- # https://gitlab.com/gitlab-org/gitlab-pages/-/issues/467
- #
- # expect(response).to match_response_schema('internal/serverless/virtual_domain')
- # expect(json_response['certificate']).to eq(pages_domain.certificate)
- # expect(json_response['key']).to eq(pages_domain.key)
- #
- # expect(json_response['lookup_paths']).to eq(
- # [
- # {
- # 'source' => {
- # 'type' => 'serverless',
- # 'service' => "test-function.#{project.name}-#{project.id}-#{environment.slug}.#{serverless_domain_cluster.knative.hostname}",
- # 'cluster' => {
- # 'hostname' => serverless_domain_cluster.knative.hostname,
- # 'address' => serverless_domain_cluster.knative.external_ip,
- # 'port' => 443,
- # 'cert' => serverless_domain_cluster.certificate,
- # 'key' => serverless_domain_cluster.key
- # }
- # }
- # }
- # ]
- # )
+ context 'when there are no pages deployed for the related project' do
+ before do
+ project.mark_pages_as_not_deployed
end
- end
- end
- context 'custom domain' do
- let(:namespace) { create(:namespace, name: 'gitlab-org') }
- let(:project) { create(:project, namespace: namespace, name: 'gitlab-ce') }
- let!(:pages_domain) { create(:pages_domain, domain: 'pages.io', project: project) }
-
- context 'when there are no pages deployed for the related project' do
it 'responds with 204 No Content' do
- query_host('pages.io')
+ get api('/internal/pages'), headers: auth_header, params: { host: 'pages.io' }
expect(response).to have_gitlab_http_status(:no_content)
end
end
context 'when there are pages deployed for the related project' do
- it 'domain lookup is case insensitive' do
- deploy_pages(project)
+ before do
+ project.mark_pages_as_deployed
+ end
- query_host('Pages.IO')
+ it 'domain lookup is case insensitive' do
+ get api('/internal/pages'), headers: auth_header, params: { host: 'Pages.IO' }
expect(response).to have_gitlab_http_status(:ok)
end
it 'responds with the correct domain configuration' do
- deploy_pages(project)
-
- query_host('pages.io')
+ get api('/internal/pages'), headers: auth_header, params: { host: 'pages.io' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('internal/pages/virtual_domain')
@@ -212,7 +116,9 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
'sha256' => deployment.file_sha256,
'file_size' => deployment.size,
'file_count' => deployment.file_count
- }
+ },
+ 'unique_host' => nil,
+ 'root_directory' => deployment.root_directory
}
]
)
@@ -220,20 +126,67 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
end
end
- context 'namespaced domain' do
- let(:group) { create(:group, name: 'mygroup') }
+ context 'when querying a unique domain' do
+ before_all do
+ project.project_setting.update!(
+ pages_unique_domain: 'unique-domain',
+ pages_unique_domain_enabled: true
+ )
+ end
- before do
- allow(Settings.pages).to receive(:host).and_return('gitlab-pages.io')
- allow(Gitlab.config.pages).to receive(:url).and_return("http://gitlab-pages.io")
+ context 'when there are no pages deployed for the related project' do
+ before do
+ project.mark_pages_as_not_deployed
+ end
+
+ it 'responds with 204 No Content' do
+ get api('/internal/pages'), headers: auth_header, params: { host: 'unique-domain.example.com' }
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
end
- context 'regular project' do
- it 'responds with the correct domain configuration' do
- project = create(:project, group: group, name: 'myproject')
- deploy_pages(project)
+ context 'when there are pages deployed for the related project' do
+ before do
+ project.mark_pages_as_deployed
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: false)
+ end
- query_host('mygroup.gitlab-pages.io')
+ context 'when there are no pages deployed for the related project' do
+ it 'responds with 204 No Content' do
+ get api('/internal/pages'), headers: auth_header, params: { host: 'unique-domain.example.com' }
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+ end
+
+ context 'when the unique domain is disabled' do
+ before do
+ project.project_setting.update!(pages_unique_domain_enabled: false)
+ end
+
+ context 'when there are no pages deployed for the related project' do
+ it 'responds with 204 No Content' do
+ get api('/internal/pages'), headers: auth_header, params: { host: 'unique-domain.example.com' }
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+ end
+
+ it 'domain lookup is case insensitive' do
+ get api('/internal/pages'), headers: auth_header, params: { host: 'Unique-Domain.example.com' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'responds with the correct domain configuration' do
+ get api('/internal/pages'), headers: auth_header, params: { host: 'unique-domain.example.com' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('internal/pages/virtual_domain')
@@ -245,7 +198,7 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
'project_id' => project.id,
'access_control' => false,
'https_only' => false,
- 'prefix' => '/myproject/',
+ 'prefix' => '/',
'source' => {
'type' => 'zip',
'path' => deployment.file.url(expire_at: 1.day.from_now),
@@ -253,56 +206,119 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do
'sha256' => deployment.file_sha256,
'file_size' => deployment.size,
'file_count' => deployment.file_count
- }
+ },
+ 'unique_host' => 'unique-domain.example.com',
+ 'root_directory' => 'public'
}
]
)
end
end
+ end
- it 'avoids N+1 queries' do
- project = create(:project, group: group)
- deploy_pages(project)
-
- control = ActiveRecord::QueryRecorder.new { query_host('mygroup.gitlab-pages.io') }
+ context 'when querying a namespaced domain' do
+ before do
+ allow(Settings.pages).to receive(:host).and_return('gitlab-pages.io')
+ allow(Gitlab.config.pages).to receive(:url).and_return("http://gitlab-pages.io")
+ end
- 3.times do
- project = create(:project, group: group)
- deploy_pages(project)
+ context 'when there are no pages deployed for the related project' do
+ before do
+ project.mark_pages_as_not_deployed
end
- expect { query_host('mygroup.gitlab-pages.io') }.not_to exceed_query_limit(control)
+ it 'responds with 204 No Content' do
+ get api('/internal/pages'), headers: auth_header, params: { host: "#{group.path}.gitlab-pages.io" }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('internal/pages/virtual_domain')
+ expect(json_response['lookup_paths']).to eq([])
+ end
end
- context 'group root project' do
- it 'responds with the correct domain configuration' do
- project = create(:project, group: group, name: 'mygroup.gitlab-pages.io')
- deploy_pages(project)
+ context 'when there are pages deployed for the related project' do
+ before do
+ project.mark_pages_as_deployed
+ end
- query_host('mygroup.gitlab-pages.io')
+ context 'with a regular project' do
+ it 'responds with the correct domain configuration' do
+ get api('/internal/pages'), headers: auth_header, params: { host: "#{group.path}.gitlab-pages.io" }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('internal/pages/virtual_domain')
+
+ deployment = project.pages_metadatum.pages_deployment
+ expect(json_response['lookup_paths']).to eq(
+ [
+ {
+ 'project_id' => project.id,
+ 'access_control' => false,
+ 'https_only' => false,
+ 'prefix' => "/#{project.path}/",
+ 'source' => {
+ 'type' => 'zip',
+ 'path' => deployment.file.url(expire_at: 1.day.from_now),
+ 'global_id' => "gid://gitlab/PagesDeployment/#{deployment.id}",
+ 'sha256' => deployment.file_sha256,
+ 'file_size' => deployment.size,
+ 'file_count' => deployment.file_count
+ },
+ 'unique_host' => nil,
+ 'root_directory' => 'public'
+ }
+ ]
+ )
+ end
+ end
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('internal/pages/virtual_domain')
+ it 'avoids N+1 queries' do
+ control = ActiveRecord::QueryRecorder.new do
+ get api('/internal/pages'), headers: auth_header, params: { host: "#{group.path}.gitlab-pages.io" }
+ end
- deployment = project.pages_metadatum.pages_deployment
- expect(json_response['lookup_paths']).to eq(
- [
- {
- 'project_id' => project.id,
- 'access_control' => false,
- 'https_only' => false,
- 'prefix' => '/',
- 'source' => {
- 'type' => 'zip',
- 'path' => deployment.file.url(expire_at: 1.day.from_now),
- 'global_id' => "gid://gitlab/PagesDeployment/#{deployment.id}",
- 'sha256' => deployment.file_sha256,
- 'file_size' => deployment.size,
- 'file_count' => deployment.file_count
+ 3.times do
+ project = create(:project, group: group)
+ project.mark_pages_as_deployed
+ end
+
+ expect { get api('/internal/pages'), headers: auth_header, params: { host: "#{group.path}.gitlab-pages.io" } }
+ .not_to exceed_query_limit(control)
+ end
+
+ context 'with a group root project' do
+ before do
+ project.update!(path: "#{group.path}.gitlab-pages.io")
+ end
+
+ it 'responds with the correct domain configuration' do
+ get api('/internal/pages'), headers: auth_header, params: { host: "#{group.path}.gitlab-pages.io" }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('internal/pages/virtual_domain')
+
+ deployment = project.pages_metadatum.pages_deployment
+ expect(json_response['lookup_paths']).to eq(
+ [
+ {
+ 'project_id' => project.id,
+ 'access_control' => false,
+ 'https_only' => false,
+ 'prefix' => '/',
+ 'source' => {
+ 'type' => 'zip',
+ 'path' => deployment.file.url(expire_at: 1.day.from_now),
+ 'global_id' => "gid://gitlab/PagesDeployment/#{deployment.id}",
+ 'sha256' => deployment.file_sha256,
+ 'file_size' => deployment.size,
+ 'file_count' => deployment.file_count
+ },
+ 'unique_host' => nil,
+ 'root_directory' => 'public'
}
- }
- ]
- )
+ ]
+ )
+ end
end
end
end
diff --git a/spec/requests/api/internal/workhorse_spec.rb b/spec/requests/api/internal/workhorse_spec.rb
index 99d0ecabbb7..2657abffae6 100644
--- a/spec/requests/api/internal/workhorse_spec.rb
+++ b/spec/requests/api/internal/workhorse_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Internal::Workhorse, :allow_forgery_protection, feature_category: :not_owned do
+RSpec.describe API::Internal::Workhorse, :allow_forgery_protection, feature_category: :shared do
include WorkhorseHelpers
context '/authorize_upload' do
diff --git a/spec/requests/api/issue_links_spec.rb b/spec/requests/api/issue_links_spec.rb
index 40d8f6d2395..fcb199a91a4 100644
--- a/spec/requests/api/issue_links_spec.rb
+++ b/spec/requests/api/issue_links_spec.rb
@@ -87,7 +87,7 @@ RSpec.describe API::IssueLinks, feature_category: :team_planning do
end
context 'when user does not have write access to given issue' do
- it 'returns 404' do
+ it 'returns 403' do
unauthorized_project = create(:project)
target_issue = create(:issue, project: unauthorized_project)
unauthorized_project.add_guest(user)
@@ -95,8 +95,8 @@ RSpec.describe API::IssueLinks, feature_category: :team_planning do
post api("/projects/#{project.id}/issues/#{issue.iid}/links", user),
params: { target_project_id: unauthorized_project.id, target_issue_iid: target_issue.iid }
- expect(response).to have_gitlab_http_status(:not_found)
- expect(json_response['message']).to eq('No matching issue found. Make sure that you are adding a valid issue URL.')
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(json_response['message']).to eq("Couldn't link issue. You must have at least the Reporter role in both projects.")
end
end
diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb
index 0641c2135c1..eaa3c46d0ca 100644
--- a/spec/requests/api/issues/get_group_issues_spec.rb
+++ b/spec/requests/api/issues/get_group_issues_spec.rb
@@ -74,7 +74,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let(:base_url) { "/groups/#{group.id}/issues" }
shared_examples 'group issues statistics' do
- it 'returns issues statistics' do
+ it 'returns issues statistics', :aggregate_failures do
get api("/groups/#{group.id}/issues_statistics", user), params: params
expect(response).to have_gitlab_http_status(:ok)
@@ -346,7 +346,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
group_project.add_reporter(user)
end
- it 'exposes known attributes' do
+ it 'exposes known attributes', :aggregate_failures do
get api(base_url, admin)
expect(response).to have_gitlab_http_status(:ok)
@@ -355,7 +355,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
it 'returns all group issues (including opened and closed)' do
- get api(base_url, admin)
+ get api(base_url, admin, admin_mode: true)
expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
end
@@ -385,7 +385,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
it 'returns group confidential issues for admin' do
- get api(base_url, admin), params: { state: :opened }
+ get api(base_url, admin, admin_mode: true), params: { state: :opened }
expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
end
@@ -403,7 +403,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'labels parameter' do
- it 'returns an array of labeled group issues' do
+ it 'returns an array of labeled group issues', :aggregate_failures do
get api(base_url, user), params: { labels: group_label.title }
expect_paginated_array_response(group_issue.id)
@@ -486,7 +486,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'returns an array of issues found by iids' do
+ it 'returns an array of issues found by iids', :aggregate_failures do
get api(base_url, user), params: { iids: [group_issue.iid] }
expect_paginated_array_response(group_issue.id)
@@ -505,14 +505,14 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect_paginated_array_response([])
end
- it 'returns an array of group issues with any label' do
+ it 'returns an array of group issues with any label', :aggregate_failures do
get api(base_url, user), params: { labels: IssuableFinder::Params::FILTER_ANY }
expect_paginated_array_response(group_issue.id)
expect(json_response.first['id']).to eq(group_issue.id)
end
- it 'returns an array of group issues with any label with labels param as array' do
+ it 'returns an array of group issues with any label with labels param as array', :aggregate_failures do
get api(base_url, user), params: { labels: [IssuableFinder::Params::FILTER_ANY] }
expect_paginated_array_response(group_issue.id)
@@ -555,7 +555,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect_paginated_array_response(group_closed_issue.id)
end
- it 'returns an array of issues with no milestone' do
+ it 'returns an array of issues with no milestone', :aggregate_failures do
get api(base_url, user), params: { milestone: no_milestone_title }
expect(response).to have_gitlab_http_status(:ok)
@@ -688,28 +688,28 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let!(:issue2) { create(:issue, author: user2, project: group_project, created_at: 2.days.ago) }
let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: group_project, created_at: 1.day.ago) }
- it 'returns issues with by assignee_username' do
+ it 'returns issues with by assignee_username', :aggregate_failures do
get api(base_url, user), params: { assignee_username: [assignee.username], scope: 'all' }
expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
expect_paginated_array_response([issue3.id, group_confidential_issue.id])
end
- it 'returns issues by assignee_username as string' do
+ it 'returns issues by assignee_username as string', :aggregate_failures do
get api(base_url, user), params: { assignee_username: assignee.username, scope: 'all' }
expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
expect_paginated_array_response([issue3.id, group_confidential_issue.id])
end
- it 'returns error when multiple assignees are passed' do
+ it 'returns error when multiple assignees are passed', :aggregate_failures do
get api(base_url, user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response["error"]).to include("allows one value, but found 2")
end
- it 'returns error when assignee_username and assignee_id are passed together' do
+ it 'returns error when assignee_username and assignee_id are passed together', :aggregate_failures do
get api(base_url, user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -719,7 +719,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
describe "#to_reference" do
- it 'exposes reference path in context of group' do
+ it 'exposes reference path in context of group', :aggregate_failures do
get api(base_url, user)
expect(json_response.first['references']['short']).to eq("##{group_closed_issue.iid}")
@@ -735,7 +735,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
group_closed_issue.reload
end
- it 'exposes reference path in context of parent group' do
+ it 'exposes reference path in context of parent group', :aggregate_failures do
get api("/groups/#{parent_group.id}/issues")
expect(json_response.first['references']['short']).to eq("##{group_closed_issue.iid}")
diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb
index 6fc3903103b..137fba66eaa 100644
--- a/spec/requests/api/issues/get_project_issues_spec.rb
+++ b/spec/requests/api/issues/get_project_issues_spec.rb
@@ -99,7 +99,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
shared_examples 'project issues statistics' do
- it 'returns project issues statistics' do
+ it 'returns project issues statistics', :aggregate_failures do
get api("/projects/#{project.id}/issues_statistics", current_user), params: params
expect(response).to have_gitlab_http_status(:ok)
@@ -317,7 +317,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
it 'returns project confidential issues for admin' do
- get api("#{base_url}/issues", admin)
+ get api("#{base_url}/issues", admin, admin_mode: true)
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
end
@@ -526,7 +526,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id])
end
- it 'exposes known attributes' do
+ it 'exposes known attributes', :aggregate_failures do
get api("#{base_url}/issues", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -607,28 +607,28 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let!(:issue2) { create(:issue, author: user2, project: project, created_at: 2.days.ago) }
let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: project, created_at: 1.day.ago) }
- it 'returns issues by assignee_username' do
+ it 'returns issues by assignee_username', :aggregate_failures do
get api("/issues", user), params: { assignee_username: [assignee.username], scope: 'all' }
expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
expect_paginated_array_response([confidential_issue.id, issue3.id])
end
- it 'returns issues by assignee_username as string' do
+ it 'returns issues by assignee_username as string', :aggregate_failures do
get api("/issues", user), params: { assignee_username: assignee.username, scope: 'all' }
expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
expect_paginated_array_response([confidential_issue.id, issue3.id])
end
- it 'returns error when multiple assignees are passed' do
+ it 'returns error when multiple assignees are passed', :aggregate_failures do
get api("/issues", user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response["error"]).to include("allows one value, but found 2")
end
- it 'returns error when assignee_username and assignee_id are passed together' do
+ it 'returns error when assignee_username and assignee_id are passed together', :aggregate_failures do
get api("/issues", user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -638,6 +638,12 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
describe 'GET /projects/:id/issues/:issue_iid' do
+ let(:path) { "/projects/#{project.id}/issues/#{confidential_issue.iid}" }
+
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:failed_status_code) { :not_found }
+ end
+
context 'when unauthenticated' do
it 'returns public issues' do
get api("/projects/#{project.id}/issues/#{issue.iid}")
@@ -646,7 +652,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'exposes known attributes' do
+ it 'exposes known attributes', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -686,7 +692,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'exposes the closed_at attribute' do
+ it 'exposes the closed_at attribute', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{closed_issue.iid}", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -694,7 +700,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'links exposure' do
- it 'exposes related resources full URIs' do
+ it 'exposes related resources full URIs', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
links = json_response['_links']
@@ -706,7 +712,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'returns a project issue by internal id' do
+ it 'returns a project issue by internal id', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -727,43 +733,43 @@ RSpec.describe API::Issues, feature_category: :team_planning do
context 'confidential issues' do
it 'returns 404 for non project members' do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member)
+ get api(path, non_member)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns 404 for project members with guest role' do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest)
+ get api(path, guest)
expect(response).to have_gitlab_http_status(:not_found)
end
- it 'returns confidential issue for project members' do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user)
+ it 'returns confidential issue for project members', :aggregate_failures do
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
- it 'returns confidential issue for author' do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author)
+ it 'returns confidential issue for author', :aggregate_failures do
+ get api(path, author)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
- it 'returns confidential issue for assignee' do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", assignee)
+ it 'returns confidential issue for assignee', :aggregate_failures do
+ get api(path, assignee)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
- it 'returns confidential issue for admin' do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin)
+ it 'returns confidential issue for admin', :aggregate_failures do
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(confidential_issue.title)
@@ -829,7 +835,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let!(:related_mr) { create_referencing_mr(user, project, issue) }
context 'when unauthenticated' do
- it 'return list of referenced merge requests from issue' do
+ it 'return list of referenced merge requests from issue', :aggregate_failures do
get_related_merge_requests(project.id, issue.iid)
expect_paginated_array_response(related_mr.id)
@@ -890,6 +896,10 @@ RSpec.describe API::Issues, feature_category: :team_planning do
describe 'GET /projects/:id/issues/:issue_iid/user_agent_detail' do
let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) }
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:path) { "/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail" }
+ end
+
context 'when unauthenticated' do
it 'returns unauthorized' do
get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail")
@@ -898,8 +908,8 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'exposes known attributes' do
- get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin)
+ it 'exposes known attributes', :aggregate_failures do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['user_agent']).to eq(user_agent_detail.user_agent)
@@ -936,7 +946,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
)
end
- it 'returns a full list of participants' do
+ it 'returns a full list of participants', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{issue.iid}/participants", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -945,7 +955,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'when user cannot see a confidential note' do
- it 'returns a limited list of participants' do
+ it 'returns a limited list of participants', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{issue.iid}/participants", create(:user))
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index 4b60eaadcbc..af289352778 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -78,7 +78,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
shared_examples 'issues statistics' do
- it 'returns issues statistics' do
+ it 'returns issues statistics', :aggregate_failures do
get api("/issues_statistics", user), params: params
expect(response).to have_gitlab_http_status(:ok)
@@ -90,9 +90,13 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
describe 'GET /issues/:id' do
+ let(:path) { "/issues/#{issue.id}" }
+
+ it_behaves_like 'GET request permissions for admin mode'
+
context 'when unauthorized' do
it 'returns unauthorized' do
- get api("/issues/#{issue.id}")
+ get api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -101,7 +105,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
context 'when authorized' do
context 'as a normal user' do
it 'returns forbidden' do
- get api("/issues/#{issue.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -109,8 +113,8 @@ RSpec.describe API::Issues, feature_category: :team_planning do
context 'as an admin' do
context 'when issue exists' do
- it 'returns the issue' do
- get api("/issues/#{issue.id}", admin)
+ it 'returns the issue', :aggregate_failures do
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.dig('author', 'id')).to eq(issue.author.id)
@@ -121,7 +125,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
context 'when issue does not exist' do
it 'returns 404' do
- get api("/issues/0", admin)
+ get api("/issues/#{non_existing_record_id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -132,7 +136,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
describe 'GET /issues' do
context 'when unauthenticated' do
- it 'returns an array of all issues' do
+ it 'returns an array of all issues', :aggregate_failures do
get api('/issues'), params: { scope: 'all' }
expect(response).to have_gitlab_http_status(:ok)
@@ -162,14 +166,14 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(response).to have_gitlab_http_status(:unauthorized)
end
- it 'returns an array of issues matching state in milestone' do
+ it 'returns an array of issues matching state in milestone', :aggregate_failures do
get api('/issues'), params: { milestone: 'foo', scope: 'all' }
expect(response).to have_gitlab_http_status(:ok)
expect_paginated_array_response([])
end
- it 'returns an array of issues matching state in milestone' do
+ it 'returns an array of issues matching state in milestone', :aggregate_failures do
get api('/issues'), params: { milestone: milestone.title, scope: 'all' }
expect(response).to have_gitlab_http_status(:ok)
@@ -273,7 +277,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'when authenticated' do
- it 'returns an array of issues' do
+ it 'returns an array of issues', :aggregate_failures do
get api('/issues', user)
expect_paginated_array_response([issue.id, closed_issue.id])
@@ -532,7 +536,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
context 'with incident issues' do
let_it_be(:incident) { create(:incident, project: project) }
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', :aggregate_failures do
get api('/issues', user) # warm up
control = ActiveRecord::QueryRecorder.new do
@@ -553,7 +557,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
context 'with issues closed as duplicates' do
let_it_be(:dup_issue_1) { create(:issue, :closed_as_duplicate, project: project) }
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', :aggregate_failures do
get api('/issues', user) # warm up
control = ActiveRecord::QueryRecorder.new do
@@ -639,7 +643,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect_paginated_array_response([])
end
- it 'returns an array of labeled issues matching given state' do
+ it 'returns an array of labeled issues matching given state', :aggregate_failures do
get api('/issues', user), params: { labels: label.title, state: :opened }
expect_paginated_array_response(issue.id)
@@ -647,7 +651,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response.first['state']).to eq('opened')
end
- it 'returns an array of labeled issues matching given state with labels param as array' do
+ it 'returns an array of labeled issues matching given state with labels param as array', :aggregate_failures do
get api('/issues', user), params: { labels: [label.title], state: :opened }
expect_paginated_array_response(issue.id)
@@ -917,14 +921,14 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'matches V4 response schema' do
+ it 'matches V4 response schema', :aggregate_failures do
get api('/issues', user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/issues')
end
- it 'returns a related merge request count of 0 if there are no related merge requests' do
+ it 'returns a related merge request count of 0 if there are no related merge requests', :aggregate_failures do
get api('/issues', user)
expect(response).to have_gitlab_http_status(:ok)
@@ -932,7 +936,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response.first).to include('merge_requests_count' => 0)
end
- it 'returns a related merge request count > 0 if there are related merge requests' do
+ it 'returns a related merge request count > 0 if there are related merge requests', :aggregate_failures do
create(:merge_requests_closing_issues, issue: issue)
get api('/issues', user)
@@ -1013,28 +1017,28 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let!(:issue2) { create(:issue, author: user2, project: project, created_at: 2.days.ago) }
let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: project, created_at: 1.day.ago) }
- it 'returns issues with by assignee_username' do
+ it 'returns issues with by assignee_username', :aggregate_failures do
get api("/issues", user), params: { assignee_username: [assignee.username], scope: 'all' }
expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
expect_paginated_array_response([confidential_issue.id, issue3.id])
end
- it 'returns issues by assignee_username as string' do
+ it 'returns issues by assignee_username as string', :aggregate_failures do
get api("/issues", user), params: { assignee_username: assignee.username, scope: 'all' }
expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
expect_paginated_array_response([confidential_issue.id, issue3.id])
end
- it 'returns error when multiple assignees are passed' do
+ it 'returns error when multiple assignees are passed', :aggregate_failures do
get api("/issues", user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response["error"]).to include("allows one value, but found 2")
end
- it 'returns error when assignee_username and assignee_id are passed together' do
+ it 'returns error when assignee_username and assignee_id are passed together', :aggregate_failures do
get api("/issues", user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -1088,7 +1092,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
describe 'GET /projects/:id/issues/:issue_iid' do
- it 'exposes full reference path' do
+ it 'exposes full reference path', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -1106,7 +1110,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'user does not have permission to view new issue' do
- it 'does not return the issue as closed_as_duplicate_of' do
+ it 'does not return the issue as closed_as_duplicate_of', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{issue_closed_as_dup.iid}", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -1119,7 +1123,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
new_issue.project.add_guest(user)
end
- it 'returns the issue as closed_as_duplicate_of' do
+ it 'returns the issue as closed_as_duplicate_of', :aggregate_failures do
get api("/projects/#{project.id}/issues/#{issue_closed_as_dup.iid}", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -1131,7 +1135,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
describe "POST /projects/:id/issues" do
- it 'creates a new project issue' do
+ it 'creates a new project issue', :aggregate_failures do
post api("/projects/#{project.id}/issues", user), params: { title: 'new issue' }
expect(response).to have_gitlab_http_status(:created)
@@ -1139,6 +1143,15 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['issue_type']).to eq('issue')
end
+ context 'when confidential is null' do
+ it 'responds with 400 error', :aggregate_failures do
+ post api("/projects/#{project.id}/issues", user), params: { title: 'issue', confidential: nil }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('confidential is empty')
+ end
+ end
+
context 'when issue create service returns an unrecoverable error' do
before do
allow_next_instance_of(Issues::CreateService) do |create_service|
@@ -1146,7 +1159,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'returns and error message and status code from the service' do
+ it 'returns and error message and status code from the service', :aggregate_failures do
post api("/projects/#{project.id}/issues", user), params: { title: 'new issue' }
expect(response).to have_gitlab_http_status(:forbidden)
@@ -1160,6 +1173,11 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let(:entity) { issue }
end
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:path) { "/projects/#{project.id}/issues/#{issue.iid}" }
+ let(:params) { { labels: 'label1', updated_at: Time.new(2000, 1, 1) } }
+ end
+
describe 'updated_at param' do
let(:fixed_time) { Time.new(2001, 1, 1) }
let(:updated_at) { Time.new(2000, 1, 1) }
@@ -1168,15 +1186,15 @@ RSpec.describe API::Issues, feature_category: :team_planning do
travel_to fixed_time
end
- it 'allows admins to set the timestamp' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", admin), params: { labels: 'label1', updated_at: updated_at }
+ it 'allows admins to set the timestamp', :aggregate_failures do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", admin, admin_mode: true), params: { labels: 'label1', updated_at: updated_at }
expect(response).to have_gitlab_http_status(:ok)
expect(Time.parse(json_response['updated_at'])).to be_like_time(updated_at)
expect(ResourceLabelEvent.last.created_at).to be_like_time(updated_at)
end
- it 'does not allow other users to set the timestamp' do
+ it 'does not allow other users to set the timestamp', :aggregate_failures do
reporter = create(:user)
project.add_developer(reporter)
@@ -1192,7 +1210,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
it 'allows issue type to be converted' do
put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { issue_type: 'incident' }
- expect(issue.reload.incident?).to be(true)
+ expect(issue.reload.work_item_type.incident?).to be(true)
end
end
end
@@ -1259,7 +1277,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
context 'with valid params' do
- it 'reorders issues and returns a successful 200 response' do
+ it 'reorders issues and returns a successful 200 response', :aggregate_failures do
put api("/projects/#{project.id}/issues/#{issue1.iid}/reorder", user), params: { move_after_id: issue2.id, move_before_id: issue3.id }
expect(response).to have_gitlab_http_status(:ok)
@@ -1286,7 +1304,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let(:other_project) { create(:project, group: group) }
let(:other_issue) { create(:issue, project: other_project, relative_position: 80) }
- it 'reorders issues and returns a successful 200 response' do
+ it 'reorders issues and returns a successful 200 response', :aggregate_failures do
put api("/projects/#{other_project.id}/issues/#{other_issue.iid}/reorder", user), params: { move_after_id: issue2.id, move_before_id: issue3.id }
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb
index 265091fa698..5a15a0b6dad 100644
--- a/spec/requests/api/issues/post_projects_issues_spec.rb
+++ b/spec/requests/api/issues/post_projects_issues_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Issues, feature_category: :team_planning do
+RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) do
create(:project, :public, creator_id: user.id, namespace: user.namespace)
@@ -123,7 +123,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
context 'an internal ID is provided' do
context 'by an admin' do
it 'sets the internal ID on the new issue' do
- post api("/projects/#{project.id}/issues", admin),
+ post api("/projects/#{project.id}/issues", admin, admin_mode: true),
params: { title: 'new issue', iid: 9001 }
expect(response).to have_gitlab_http_status(:created)
@@ -167,7 +167,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
context 'when an issue with the same IID exists on database' do
it 'returns 409' do
- post api("/projects/#{project.id}/issues", admin),
+ post api("/projects/#{project.id}/issues", admin, admin_mode: true),
params: { title: 'new issue', iid: issue.iid }
expect(response).to have_gitlab_http_status(:conflict)
@@ -337,7 +337,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
context 'by an admin' do
it 'sets the creation time on the new issue' do
- post api("/projects/#{project.id}/issues", admin), params: params
+ post api("/projects/#{project.id}/issues", admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:created)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
@@ -475,9 +475,15 @@ RSpec.describe API::Issues, feature_category: :team_planning do
describe '/projects/:id/issues/:issue_iid/move' do
let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace) }
+ let(:path) { "/projects/#{project.id}/issues/#{issue.iid}/move" }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { { to_project_id: target_project2.id } }
+ let(:failed_status_code) { 400 }
+ end
it 'moves an issue' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
+ post api(path, user),
params: { to_project_id: target_project.id }
expect(response).to have_gitlab_http_status(:created)
@@ -486,7 +492,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
context 'when source and target projects are the same' do
it 'returns 400 when trying to move an issue' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
+ post api(path, user),
params: { to_project_id: project.id }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -496,7 +502,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
context 'when the user does not have the permission to move issues' do
it 'returns 400 when trying to move an issue' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
+ post api(path, user),
params: { to_project_id: target_project2.id }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -505,7 +511,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
it 'moves the issue to another namespace if I am admin' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/move", admin),
+ post api(path, admin, admin_mode: true),
params: { to_project_id: target_project2.id }
expect(response).to have_gitlab_http_status(:created)
@@ -544,7 +550,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
context 'when target project does not exist' do
it 'returns 404 when trying to move an issue' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
+ post api(path, user),
params: { to_project_id: 0 }
expect(response).to have_gitlab_http_status(:not_found)
diff --git a/spec/requests/api/issues/put_projects_issues_spec.rb b/spec/requests/api/issues/put_projects_issues_spec.rb
index f0d174c9e78..217788c519f 100644
--- a/spec/requests/api/issues/put_projects_issues_spec.rb
+++ b/spec/requests/api/issues/put_projects_issues_spec.rb
@@ -80,7 +80,12 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
describe 'PUT /projects/:id/issues/:issue_iid to update only title' do
- it 'updates a project issue' do
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:path) { "/projects/#{project.id}/issues/#{confidential_issue.iid}" }
+ let(:params) { { title: updated_title } }
+ end
+
+ it 'updates a project issue', :aggregate_failures do
put api_for_user, params: { title: updated_title }
expect(response).to have_gitlab_http_status(:ok)
@@ -88,7 +93,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
it 'returns 404 error if issue iid not found' do
- put api("/projects/#{project.id}/issues/44444", user), params: { title: updated_title }
+ put api("/projects/#{project.id}/issues/#{non_existing_record_id}", user), params: { title: updated_title }
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -109,7 +114,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(response).to have_gitlab_http_status(:ok)
end
- it 'allows special label names with labels param as array' do
+ it 'allows special label names with labels param as array', :aggregate_failures do
put api_for_user,
params: {
title: updated_title,
@@ -135,42 +140,42 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(response).to have_gitlab_http_status(:forbidden)
end
- it 'updates a confidential issue for project members' do
+ it 'updates a confidential issue for project members', :aggregate_failures do
put api(confidential_issue_path, user), params: { title: updated_title }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(updated_title)
end
- it 'updates a confidential issue for author' do
+ it 'updates a confidential issue for author', :aggregate_failures do
put api(confidential_issue_path, author), params: { title: updated_title }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(updated_title)
end
- it 'updates a confidential issue for admin' do
- put api(confidential_issue_path, admin), params: { title: updated_title }
+ it 'updates a confidential issue for admin', :aggregate_failures do
+ put api(confidential_issue_path, admin, admin_mode: true), params: { title: updated_title }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(updated_title)
end
- it 'sets an issue to confidential' do
+ it 'sets an issue to confidential', :aggregate_failures do
put api_for_user, params: { confidential: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['confidential']).to be_truthy
end
- it 'makes a confidential issue public' do
+ it 'makes a confidential issue public', :aggregate_failures do
put api(confidential_issue_path, user), params: { confidential: false }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['confidential']).to be_falsy
end
- it 'does not update a confidential issue with wrong confidential flag' do
+ it 'does not update a confidential issue with wrong confidential flag', :aggregate_failures do
put api(confidential_issue_path, user), params: { confidential: 'foo' }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -209,7 +214,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect { update_issue }.not_to change { issue.reload.title }
end
- it 'returns correct status and message' do
+ it 'returns correct status and message', :aggregate_failures do
update_issue
expect(response).to have_gitlab_http_status(:bad_request)
@@ -246,14 +251,14 @@ RSpec.describe API::Issues, feature_category: :team_planning do
describe 'PUT /projects/:id/issues/:issue_iid to update assignee' do
context 'support for deprecated assignee_id' do
- it 'removes assignee' do
+ it 'removes assignee', :aggregate_failures do
put api_for_user, params: { assignee_id: 0 }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['assignee']).to be_nil
end
- it 'updates an issue with new assignee' do
+ it 'updates an issue with new assignee', :aggregate_failures do
put api_for_user, params: { assignee_id: user2.id }
expect(response).to have_gitlab_http_status(:ok)
@@ -261,21 +266,21 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'removes assignee' do
+ it 'removes assignee', :aggregate_failures do
put api_for_user, params: { assignee_ids: [0] }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['assignees']).to be_empty
end
- it 'updates an issue with new assignee' do
+ it 'updates an issue with new assignee', :aggregate_failures do
put api_for_user, params: { assignee_ids: [user2.id] }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['assignees'].first['name']).to eq(user2.name)
end
- context 'single assignee restrictions' do
+ context 'single assignee restrictions', :aggregate_failures do
it 'updates an issue with several assignees but only one has been applied' do
put api_for_user, params: { assignee_ids: [user2.id, guest.id] }
@@ -289,7 +294,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let!(:label) { create(:label, title: 'dummy', project: project) }
let!(:label_link) { create(:label_link, label: label, target: issue) }
- it 'adds relevant labels' do
+ it 'adds relevant labels', :aggregate_failures do
put api_for_user, params: { add_labels: '1, 2' }
expect(response).to have_gitlab_http_status(:ok)
@@ -300,14 +305,14 @@ RSpec.describe API::Issues, feature_category: :team_planning do
let!(:label2) { create(:label, title: 'a-label', project: project) }
let!(:label_link2) { create(:label_link, label: label2, target: issue) }
- it 'removes relevant labels' do
+ it 'removes relevant labels', :aggregate_failures do
put api_for_user, params: { remove_labels: label2.title }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to eq([label.title])
end
- it 'removes all labels' do
+ it 'removes all labels', :aggregate_failures do
put api_for_user, params: { remove_labels: "#{label.title}, #{label2.title}" }
expect(response).to have_gitlab_http_status(:ok)
@@ -315,14 +320,14 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
end
- it 'does not update labels if not present' do
+ it 'does not update labels if not present', :aggregate_failures do
put api_for_user, params: { title: updated_title }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to eq([label.title])
end
- it 'removes all labels and touches the record' do
+ it 'removes all labels and touches the record', :aggregate_failures do
travel_to(2.minutes.from_now) do
put api_for_user, params: { labels: '' }
end
@@ -332,7 +337,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['updated_at']).to be > Time.current
end
- it 'removes all labels and touches the record with labels param as array' do
+ it 'removes all labels and touches the record with labels param as array', :aggregate_failures do
travel_to(2.minutes.from_now) do
put api_for_user, params: { labels: [''] }
end
@@ -342,7 +347,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['updated_at']).to be > Time.current
end
- it 'updates labels and touches the record' do
+ it 'updates labels and touches the record', :aggregate_failures do
travel_to(2.minutes.from_now) do
put api_for_user, params: { labels: 'foo,bar' }
end
@@ -352,7 +357,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['updated_at']).to be > Time.current
end
- it 'updates labels and touches the record with labels param as array' do
+ it 'updates labels and touches the record with labels param as array', :aggregate_failures do
travel_to(2.minutes.from_now) do
put api_for_user, params: { labels: %w(foo bar) }
end
@@ -363,21 +368,21 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['updated_at']).to be > Time.current
end
- it 'allows special label names' do
+ it 'allows special label names', :aggregate_failures do
put api_for_user, params: { labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to contain_exactly('label:foo', 'label-bar', 'label_bar', 'label/bar', 'label?bar', 'label&bar', '?', '&')
end
- it 'allows special label names with labels param as array' do
+ it 'allows special label names with labels param as array', :aggregate_failures do
put api_for_user, params: { labels: ['label:foo', 'label-bar', 'label_bar', 'label/bar,label?bar,label&bar,?,&'] }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['labels']).to contain_exactly('label:foo', 'label-bar', 'label_bar', 'label/bar', 'label?bar', 'label&bar', '?', '&')
end
- it 'returns 400 if title is too long' do
+ it 'returns 400 if title is too long', :aggregate_failures do
put api_for_user, params: { title: 'g' * 256 }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -386,7 +391,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
describe 'PUT /projects/:id/issues/:issue_iid to update state and label' do
- it 'updates a project issue' do
+ it 'updates a project issue', :aggregate_failures do
put api_for_user, params: { labels: 'label2', state_event: 'close' }
expect(response).to have_gitlab_http_status(:ok)
@@ -394,7 +399,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(json_response['state']).to eq 'closed'
end
- it 'reopens a project isssue' do
+ it 'reopens a project isssue', :aggregate_failures do
put api(issue_path, user), params: { state_event: 'reopen' }
expect(response).to have_gitlab_http_status(:ok)
@@ -404,7 +409,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
describe 'PUT /projects/:id/issues/:issue_iid to update updated_at param' do
context 'when reporter makes request' do
- it 'accepts the update date to be set' do
+ it 'accepts the update date to be set', :aggregate_failures do
update_time = 2.weeks.ago
put api_for_user, params: { title: 'some new title', updated_at: update_time }
@@ -436,7 +441,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'accepts the update date to be set' do
+ it 'accepts the update date to be set', :aggregate_failures do
update_time = 2.weeks.ago
put api_for_owner, params: { title: 'some new title', updated_at: update_time }
@@ -448,7 +453,7 @@ RSpec.describe API::Issues, feature_category: :team_planning do
end
describe 'PUT /projects/:id/issues/:issue_iid to update due date' do
- it 'creates a new project issue' do
+ it 'creates a new project issue', :aggregate_failures do
due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
put api_for_user, params: { due_date: due_date }
diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb
index d9a0f061156..3f600d24891 100644
--- a/spec/requests/api/keys_spec.rb
+++ b/spec/requests/api/keys_spec.rb
@@ -2,31 +2,35 @@
require 'spec_helper'
-RSpec.describe API::Keys, feature_category: :authentication_and_authorization do
+RSpec.describe API::Keys, :aggregate_failures, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
let_it_be(:email) { create(:email, user: user) }
let_it_be(:key) { create(:rsa_key_4096, user: user, expires_at: 1.day.from_now) }
let_it_be(:fingerprint_md5) { 'df:73:db:29:3c:a5:32:cf:09:17:7e:8e:9d:de:d7:f7' }
+ let_it_be(:path) { "/keys/#{key.id}" }
describe 'GET /keys/:uid' do
+ it_behaves_like 'GET request permissions for admin mode'
+
context 'when unauthenticated' do
it 'returns authentication error' do
- get api("/keys/#{key.id}")
+ get api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'when authenticated' do
it 'returns 404 for non-existing key' do
- get api('/keys/0', admin)
+ get api('/keys/0', admin, admin_mode: true)
+
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Not found')
end
it 'returns single ssh key with user information' do
- get api("/keys/#{key.id}", admin)
- expect(response).to have_gitlab_http_status(:ok)
+ get api(path, admin, admin_mode: true)
+
expect(json_response['title']).to eq(key.title)
expect(Time.parse(json_response['expires_at'])).to be_like_time(key.expires_at)
expect(json_response['user']['id']).to eq(user.id)
@@ -34,7 +38,7 @@ RSpec.describe API::Keys, feature_category: :authentication_and_authorization do
end
it "does not include the user's `is_admin` flag" do
- get api("/keys/#{key.id}", admin)
+ get api(path, admin, admin_mode: true)
expect(json_response['user']['is_admin']).to be_nil
end
@@ -42,31 +46,28 @@ RSpec.describe API::Keys, feature_category: :authentication_and_authorization do
end
describe 'GET /keys?fingerprint=' do
- it 'returns authentication error' do
- get api("/keys?fingerprint=#{fingerprint_md5}")
+ let_it_be(:path) { "/keys?fingerprint=#{fingerprint_md5}" }
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
+ it_behaves_like 'GET request permissions for admin mode'
- it 'returns authentication error when authenticated as user' do
- get api("/keys?fingerprint=#{fingerprint_md5}", user)
+ it 'returns authentication error' do
+ get api(path, admin_mode: true)
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
context 'when authenticated as admin' do
context 'MD5 fingerprint' do
it 'returns 404 for non-existing SSH md5 fingerprint' do
- get api("/keys?fingerprint=11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11", admin)
+ get api("/keys?fingerprint=11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Key Not Found')
end
it 'returns user if SSH md5 fingerprint found' do
- get api("/keys?fingerprint=#{fingerprint_md5}", admin)
+ get api(path, admin, admin_mode: true)
- expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(key.title)
expect(json_response['user']['id']).to eq(user.id)
expect(json_response['user']['username']).to eq(user.username)
@@ -74,14 +75,14 @@ RSpec.describe API::Keys, feature_category: :authentication_and_authorization do
context 'with FIPS mode', :fips_mode do
it 'returns 404 for non-existing SSH md5 fingerprint' do
- get api("/keys?fingerprint=11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11", admin)
+ get api("/keys?fingerprint=11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq('Failed to return the key')
end
it 'returns 404 for existing SSH md5 fingerprint' do
- get api("/keys?fingerprint=#{fingerprint_md5}", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq('Failed to return the key')
@@ -90,14 +91,14 @@ RSpec.describe API::Keys, feature_category: :authentication_and_authorization do
end
it 'returns 404 for non-existing SSH sha256 fingerprint' do
- get api("/keys?fingerprint=#{URI.encode_www_form_component("SHA256:nUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo1lCg")}", admin)
+ get api("/keys?fingerprint=#{URI.encode_www_form_component("SHA256:nUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo1lCg")}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Key Not Found')
end
it 'returns user if SSH sha256 fingerprint found' do
- get api("/keys?fingerprint=#{URI.encode_www_form_component("SHA256:" + key.fingerprint_sha256)}", admin)
+ get api("/keys?fingerprint=#{URI.encode_www_form_component("SHA256:" + key.fingerprint_sha256)}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(key.title)
@@ -106,7 +107,7 @@ RSpec.describe API::Keys, feature_category: :authentication_and_authorization do
end
it 'returns user if SSH sha256 fingerprint found' do
- get api("/keys?fingerprint=#{URI.encode_www_form_component("sha256:" + key.fingerprint_sha256)}", admin)
+ get api("/keys?fingerprint=#{URI.encode_www_form_component("sha256:" + key.fingerprint_sha256)}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(key.title)
@@ -115,7 +116,7 @@ RSpec.describe API::Keys, feature_category: :authentication_and_authorization do
end
it "does not include the user's `is_admin` flag" do
- get api("/keys?fingerprint=#{URI.encode_www_form_component("sha256:" + key.fingerprint_sha256)}", admin)
+ get api("/keys?fingerprint=#{URI.encode_www_form_component("sha256:" + key.fingerprint_sha256)}", admin, admin_mode: true)
expect(json_response['user']['is_admin']).to be_nil
end
@@ -136,7 +137,7 @@ RSpec.describe API::Keys, feature_category: :authentication_and_authorization do
it 'returns user and projects if SSH sha256 fingerprint for DeployKey found' do
user.keys << deploy_key
- get api("/keys?fingerprint=#{URI.encode_www_form_component("SHA256:" + deploy_key.fingerprint_sha256)}", admin)
+ get api("/keys?fingerprint=#{URI.encode_www_form_component("SHA256:" + deploy_key.fingerprint_sha256)}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(deploy_key.title)
diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb
index 82b87007a9b..05a9d98a9d0 100644
--- a/spec/requests/api/lint_spec.rb
+++ b/spec/requests/api/lint_spec.rb
@@ -2,224 +2,14 @@
require 'spec_helper'
-RSpec.describe API::Lint, feature_category: :pipeline_authoring do
+RSpec.describe API::Lint, feature_category: :pipeline_composition do
describe 'POST /ci/lint' do
- context 'when signup settings are disabled' do
- before do
- Gitlab::CurrentSettings.signup_enabled = false
- end
-
- context 'when unauthenticated' do
- it 'returns authentication error' do
- post api('/ci/lint'), params: { content: 'content' }
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- context 'when authenticated' do
- let_it_be(:api_user) { create(:user) }
-
- it 'returns authorized' do
- post api('/ci/lint', api_user), params: { content: 'content' }
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- context 'when authenticated as external user' do
- let(:project) { create(:project) }
- let(:api_user) { create(:user, :external) }
-
- context 'when reporter in a project' do
- before do
- project.add_reporter(api_user)
- end
-
- it 'returns authorization failure' do
- post api('/ci/lint', api_user), params: { content: 'content' }
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- context 'when developer in a project' do
- before do
- project.add_developer(api_user)
- end
-
- it 'returns authorization success' do
- post api('/ci/lint', api_user), params: { content: 'content' }
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
- end
- end
+ it 'responds with a 410' do
+ user = create(:user)
- context 'when signup is enabled and not limited' do
- before do
- Gitlab::CurrentSettings.signup_enabled = true
- stub_application_setting(domain_allowlist: [], email_restrictions_enabled: false, require_admin_approval_after_user_signup: false)
- end
-
- context 'when unauthenticated' do
- it 'returns authorized success' do
- post api('/ci/lint'), params: { content: 'content' }
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- context 'when authenticated' do
- let_it_be(:api_user) { create(:user) }
-
- it 'returns authentication success' do
- post api('/ci/lint', api_user), params: { content: 'content' }
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
- end
-
- context 'when limited signup is enabled' do
- before do
- stub_application_setting(domain_allowlist: ['www.gitlab.com'])
- Gitlab::CurrentSettings.signup_enabled = true
- end
-
- context 'when unauthenticated' do
- it 'returns unauthorized' do
- post api('/ci/lint'), params: { content: 'content' }
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- context 'when authenticated' do
- let_it_be(:api_user) { create(:user) }
-
- it 'returns authentication success' do
- post api('/ci/lint', api_user), params: { content: 'content' }
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
- end
-
- context 'when authenticated' do
- let_it_be(:api_user) { create(:user) }
+ post api('/ci/lint', user), params: { content: "test_job:\n script: ls" }
- context 'with valid .gitlab-ci.yml content' do
- let(:yaml_content) do
- File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
- end
-
- it 'passes validation without warnings or errors' do
- post api('/ci/lint', api_user), params: { content: yaml_content }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an Hash
- expect(json_response['status']).to eq('valid')
- expect(json_response['warnings']).to match_array([])
- expect(json_response['errors']).to match_array([])
- expect(json_response['includes']).to eq([])
- end
-
- it 'outputs expanded yaml content' do
- post api('/ci/lint', api_user), params: { content: yaml_content, include_merged_yaml: true }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to have_key('merged_yaml')
- end
-
- it 'outputs jobs' do
- post api('/ci/lint', api_user), params: { content: yaml_content, include_jobs: true }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to have_key('jobs')
- end
- end
-
- context 'with valid .gitlab-ci.yml with warnings' do
- let(:yaml_content) { { job: { script: 'ls', rules: [{ when: 'always' }] } }.to_yaml }
-
- it 'passes validation but returns warnings' do
- post api('/ci/lint', api_user), params: { content: yaml_content }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['status']).to eq('valid')
- expect(json_response['warnings']).not_to be_empty
- expect(json_response['errors']).to match_array([])
- end
- end
-
- context 'with an invalid .gitlab-ci.yml' do
- context 'with invalid syntax' do
- let(:yaml_content) { 'invalid content' }
-
- it 'responds with errors about invalid syntax' do
- post api('/ci/lint', api_user), params: { content: yaml_content }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['status']).to eq('invalid')
- expect(json_response['warnings']).to eq([])
- expect(json_response['errors']).to eq(['Invalid configuration format'])
- expect(json_response['includes']).to eq(nil)
- end
-
- it 'outputs expanded yaml content' do
- post api('/ci/lint', api_user), params: { content: yaml_content, include_merged_yaml: true }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to have_key('merged_yaml')
- end
-
- it 'outputs jobs' do
- post api('/ci/lint', api_user), params: { content: yaml_content, include_jobs: true }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to have_key('jobs')
- end
- end
-
- context 'with invalid configuration' do
- let(:yaml_content) { '{ image: "image:1.0", services: ["postgres"] }' }
-
- it 'responds with errors about invalid configuration' do
- post api('/ci/lint', api_user), params: { content: yaml_content }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['status']).to eq('invalid')
- expect(json_response['warnings']).to eq([])
- expect(json_response['errors']).to eq(['jobs config should contain at least one visible job'])
- expect(json_response['includes']).to eq([])
- end
-
- it 'outputs expanded yaml content' do
- post api('/ci/lint', api_user), params: { content: yaml_content, include_merged_yaml: true }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to have_key('merged_yaml')
- end
-
- it 'outputs jobs' do
- post api('/ci/lint', api_user), params: { content: yaml_content, include_jobs: true }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to have_key('jobs')
- end
- end
- end
-
- context 'without the content parameter' do
- it 'responds with validation error about missing content' do
- post api('/ci/lint', api_user)
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq('content is missing')
- end
- end
+ expect(response).to have_gitlab_http_status(:gone)
end
end
@@ -245,8 +35,8 @@ RSpec.describe API::Lint, feature_category: :pipeline_authoring do
it 'passes validation' do
ci_lint
- included_config = YAML.safe_load(included_content, [Symbol])
- root_config = YAML.safe_load(yaml_content, [Symbol])
+ included_config = YAML.safe_load(included_content, permitted_classes: [Symbol])
+ root_config = YAML.safe_load(yaml_content, permitted_classes: [Symbol])
expected_yaml = included_config.merge(root_config).except(:include).deep_stringify_keys.to_yaml
expect(response).to have_gitlab_http_status(:ok)
@@ -535,8 +325,8 @@ RSpec.describe API::Lint, feature_category: :pipeline_authoring do
it 'passes validation' do
ci_lint
- included_config = YAML.safe_load(included_content, [Symbol])
- root_config = YAML.safe_load(yaml_content, [Symbol])
+ included_config = YAML.safe_load(included_content, permitted_classes: [Symbol])
+ root_config = YAML.safe_load(yaml_content, permitted_classes: [Symbol])
expected_yaml = included_config.merge(root_config).except(:include).deep_stringify_keys.to_yaml
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/requests/api/maven_packages_spec.rb b/spec/requests/api/maven_packages_spec.rb
index 20aa660d95b..60e91973b5d 100644
--- a/spec/requests/api/maven_packages_spec.rb
+++ b/spec/requests/api/maven_packages_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe API::MavenPackages, feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
include WorkhorseHelpers
+ include HttpBasicAuthHelpers
include_context 'workhorse headers'
@@ -22,7 +23,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
let_it_be(:deploy_token_for_group) { create(:deploy_token, :group, read_package_registry: true, write_package_registry: true) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token_for_group, group: group) }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_maven_user' } }
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_maven_user' } }
let(:package_name) { 'com/example/my-app' }
let(:headers) { workhorse_headers }
@@ -159,56 +160,149 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
end
end
- shared_examples 'downloads with a deploy token' do
- context 'successful download' do
+ shared_examples 'allowing the download' do
+ it 'allows download' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
+ end
+ end
+
+ shared_examples 'not allowing the download with' do |not_found_response|
+ it 'does not allow the download' do
+ subject
+
+ expect(response).to have_gitlab_http_status(not_found_response)
+ end
+ end
+
+ shared_examples 'downloads with a personal access token' do |not_found_response|
+ where(:valid, :sent_using) do
+ true | :custom_header
+ false | :custom_header
+ true | :basic_auth
+ false | :basic_auth
+ end
+
+ with_them do
+ let(:token) { valid ? personal_access_token.token : 'not_valid' }
+ let(:headers) do
+ case sent_using
+ when :custom_header
+ { 'Private-Token' => token }
+ when :basic_auth
+ basic_auth_header(user.username, token)
+ end
+ end
+
subject do
download_file(
file_name: package_file.file_name,
- request_headers: { Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token.token }
+ request_headers: headers
)
end
- it 'allows download with deploy token' do
- subject
+ if params[:valid]
+ it_behaves_like 'allowing the download'
+ else
+ expected_status_code = not_found_response
+ # invalid PAT values sent through headers are validated.
+ # Invalid values will trigger an :unauthorized response (and not set current_user to nil)
+ expected_status_code = :unauthorized if params[:sent_using] == :custom_header && !params[:valid]
+ it_behaves_like 'not allowing the download with', expected_status_code
+ end
+ end
+ end
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('application/octet-stream')
+ shared_examples 'downloads with a deploy token' do |not_found_response|
+ where(:valid, :sent_using) do
+ true | :custom_header
+ false | :custom_header
+ true | :basic_auth
+ false | :basic_auth
+ end
+
+ with_them do
+ let(:token) { valid ? deploy_token.token : 'not_valid' }
+ let(:headers) do
+ case sent_using
+ when :custom_header
+ { Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => token }
+ when :basic_auth
+ basic_auth_header(deploy_token.username, token)
+ end
end
- it 'allows download with deploy token with only write_package_registry scope' do
- deploy_token.update!(read_package_registry: false)
+ subject do
+ download_file(
+ file_name: package_file.file_name,
+ request_headers: headers
+ )
+ end
- subject
+ if params[:valid]
+ it_behaves_like 'allowing the download'
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('application/octet-stream')
+ context 'with only write_package_registry scope' do
+ it_behaves_like 'allowing the download' do
+ before do
+ deploy_token.update!(read_package_registry: false)
+ end
+ end
+ end
+ else
+ it_behaves_like 'not allowing the download with', not_found_response
end
end
end
shared_examples 'downloads with a job token' do
- context 'with a running job' do
- it 'allows download with job token' do
- download_file(file_name: package_file.file_name, params: { job_token: job.token })
+ where(:valid, :sent_using) do
+ true | :custom_params
+ false | :custom_params
+ true | :basic_auth
+ false | :basic_auth
+ end
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('application/octet-stream')
+ with_them do
+ let(:token) { valid ? job.token : 'not_valid' }
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, token) }
+ let(:params) { { job_token: token } }
+
+ subject do
+ case sent_using
+ when :custom_params
+ download_file(file_name: package_file.file_name, params: params)
+ when :basic_auth
+ download_file(file_name: package_file.file_name, request_headers: headers)
+ end
end
- end
- context 'with a finished job' do
- before do
- job.update!(status: :failed)
+ context 'with a running job' do
+ if params[:valid]
+ it_behaves_like 'allowing the download'
+ else
+ it_behaves_like 'not allowing the download with', :unauthorized
+ end
end
- it 'returns unauthorized error' do
- download_file(file_name: package_file.file_name, params: { job_token: job.token })
+ context 'with a finished job' do
+ before do
+ job.update!(status: :failed)
+ end
- expect(response).to have_gitlab_http_status(:unauthorized)
+ it_behaves_like 'not allowing the download with', :unauthorized
end
end
end
+ shared_examples 'downloads with different tokens' do |not_found_response|
+ it_behaves_like 'downloads with a personal access token', not_found_response
+ it_behaves_like 'downloads with a deploy token', not_found_response
+ it_behaves_like 'downloads with a job token'
+ end
+
shared_examples 'successfully returning the file' do
it 'returns the file', :aggregate_failures do
subject
@@ -285,6 +379,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
describe 'GET /api/v4/packages/maven/*path/:file_name' do
context 'a public project' do
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_maven_user' } }
+
subject { download_file(file_name: package_file.file_name) }
shared_examples 'getting a file' do
@@ -336,11 +432,10 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
it 'denies download when no private token' do
download_file(file_name: package_file.file_name)
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
- it_behaves_like 'downloads with a job token'
- it_behaves_like 'downloads with a deploy token'
+ it_behaves_like 'downloads with different tokens', :unauthorized
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
@@ -377,11 +472,10 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
it 'denies download when no private token' do
download_file(file_name: package_file.file_name)
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
- it_behaves_like 'downloads with a job token'
- it_behaves_like 'downloads with a deploy token'
+ it_behaves_like 'downloads with different tokens', :unauthorized
it 'does not allow download by a unauthorized deploy token with same id as a user with access' do
unauthorized_deploy_token = create(:deploy_token, read_package_registry: true, write_package_registry: true)
@@ -411,12 +505,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
end
context 'project name is different from a package name' do
- before do
- maven_metadatum.update!(path: "wrong_name/#{package.version}")
- end
-
it 'rejects request' do
- download_file(file_name: package_file.file_name)
+ download_file(file_name: package_file.file_name, path: "wrong_name/#{package.version}")
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -451,6 +541,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
it_behaves_like 'forwarding package requests'
context 'a public project' do
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_maven_user' } }
+
subject { download_file(file_name: package_file.file_name) }
shared_examples 'getting a file for a group' do
@@ -496,8 +588,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
expect(response).to have_gitlab_http_status(not_found_response)
end
- it_behaves_like 'downloads with a job token'
- it_behaves_like 'downloads with a deploy token'
+ it_behaves_like 'downloads with different tokens', not_found_response
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
@@ -506,7 +597,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
end
end
- it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { internal: :not_found, public: :redirect }
+ it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { internal: :unauthorized, public: :redirect }
end
context 'private project' do
@@ -535,8 +626,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
expect(response).to have_gitlab_http_status(not_found_response)
end
- it_behaves_like 'downloads with a job token'
- it_behaves_like 'downloads with a deploy token'
+ it_behaves_like 'downloads with different tokens', not_found_response
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
@@ -566,7 +656,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
end
end
- it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { private: :not_found, internal: :not_found, public: :redirect }
+ it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { private: :unauthorized, internal: :unauthorized, public: :redirect }
context 'with a reporter from a subgroup accessing the root group' do
let_it_be(:root_group) { create(:group, :private) }
@@ -660,6 +750,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
describe 'GET /api/v4/projects/:id/packages/maven/*path/:file_name' do
context 'a public project' do
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_maven_user' } }
+
subject { download_file(file_name: package_file.file_name) }
it_behaves_like 'tracking the file download event'
@@ -718,7 +810,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
it 'denies download when no private token' do
download_file(file_name: package_file.file_name)
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:unauthorized)
end
context 'with access to package registry for everyone' do
@@ -731,8 +823,7 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
it_behaves_like 'successfully returning the file'
end
- it_behaves_like 'downloads with a job token'
- it_behaves_like 'downloads with a deploy token'
+ it_behaves_like 'downloads with different tokens', :unauthorized
context 'with a non existing maven path' do
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
@@ -901,8 +992,6 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
it_behaves_like 'package workhorse uploads'
context 'event tracking' do
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_maven_user' } }
-
it_behaves_like 'a package tracking event', described_class.name, 'push_package'
context 'when the package file fails to be created' do
@@ -917,6 +1006,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
end
it 'creates package and stores package file' do
+ expect_use_primary
+
expect { upload_file_with_token(params: params) }.to change { project.packages.count }.by(1)
.and change { Packages::Maven::Metadatum.count }.by(1)
.and change { Packages::PackageFile.count }.by(1)
@@ -962,6 +1053,17 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
expect(response).to have_gitlab_http_status(:forbidden)
end
+ context 'file name is too long' do
+ let(:file_name) { 'a' * (Packages::Maven::FindOrCreatePackageService::MAX_FILE_NAME_LENGTH + 1) }
+
+ it 'rejects request' do
+ expect { upload_file_with_token(params: params, file_name: file_name) }.not_to change { project.packages.count }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to include('File name is too long')
+ end
+ end
+
context 'version is not correct' do
let(:version) { '$%123' }
@@ -981,9 +1083,9 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
package_settings.update!(maven_duplicates_allowed: false)
end
- shared_examples 'storing the package file' do
+ shared_examples 'storing the package file' do |file_name: 'my-app-1.0-20180724.124855-1'|
it 'stores the file', :aggregate_failures do
- expect { upload_file_with_token(params: params) }.to change { package.package_files.count }.by(1)
+ expect { upload_file_with_token(params: params, file_name: file_name) }.to change { package.package_files.count }.by(1)
expect(response).to have_gitlab_http_status(:ok)
expect(jar_file.file_name).to eq(file_upload.original_filename)
@@ -1023,6 +1125,10 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
it_behaves_like 'storing the package file'
end
+
+ context 'when uploading a similar package file name with a classifier' do
+ it_behaves_like 'storing the package file', file_name: 'my-app-1.0-20180724.124855-1-javadoc'
+ end
end
context 'for sha1 file' do
@@ -1043,6 +1149,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
end
it 'returns no content' do
+ expect_use_primary
+
upload
expect(response).to have_gitlab_http_status(:no_content)
@@ -1072,6 +1180,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
subject { upload_file_with_token(params: params, file_extension: 'jar.md5') }
it 'returns an empty body' do
+ expect_use_primary
+
subject
expect(response.body).to eq('')
@@ -1086,10 +1196,40 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
end
end
end
+
+ context 'reading fingerprints from UploadedFile instance' do
+ let(:file) { Packages::Package.last.package_files.with_format('%.jar').last }
+
+ subject { upload_file_with_token(params: params) }
+
+ before do
+ allow_next_instance_of(UploadedFile) do |uploaded_file|
+ allow(uploaded_file).to receive(:size).and_return(123)
+ allow(uploaded_file).to receive(:sha1).and_return('sha1')
+ allow(uploaded_file).to receive(:md5).and_return('md5')
+ end
+ end
+
+ it 'reads size, sha1 and md5 fingerprints from uploaded_file instance' do
+ subject
+
+ expect(file.size).to eq(123)
+ expect(file.file_sha1).to eq('sha1')
+ expect(file.file_md5).to eq('md5')
+ end
+ end
+
+ def expect_use_primary
+ lb_session = ::Gitlab::Database::LoadBalancing::Session.current
+
+ expect(lb_session).to receive(:use_primary).and_call_original
+
+ allow(::Gitlab::Database::LoadBalancing::Session).to receive(:current).and_return(lb_session)
+ end
end
- def upload_file(params: {}, request_headers: headers, file_extension: 'jar')
- url = "/projects/#{project.id}/packages/maven/#{param_path}/my-app-1.0-20180724.124855-1.#{file_extension}"
+ def upload_file(params: {}, request_headers: headers, file_extension: 'jar', file_name: 'my-app-1.0-20180724.124855-1')
+ url = "/projects/#{project.id}/packages/maven/#{param_path}/#{file_name}.#{file_extension}"
workhorse_finalize(
api(url),
method: :put,
@@ -1100,8 +1240,8 @@ RSpec.describe API::MavenPackages, feature_category: :package_registry do
)
end
- def upload_file_with_token(params: {}, request_headers: headers_with_token, file_extension: 'jar')
- upload_file(params: params, request_headers: request_headers, file_extension: file_extension)
+ def upload_file_with_token(params: {}, request_headers: headers_with_token, file_extension: 'jar', file_name: 'my-app-1.0-20180724.124855-1')
+ upload_file(params: params, request_headers: request_headers, file_name: file_name, file_extension: file_extension)
end
end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 4eff5e96e9c..353fddcb08d 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -130,6 +130,8 @@ RSpec.describe API::Members, feature_category: :subgroups do
let(:project_user) { create(:user) }
let(:linked_group_user) { create(:user) }
let!(:project_group_link) { create(:project_group_link, project: project, group: linked_group) }
+ let(:invited_group_developer) { create(:user, username: 'invited_group_developer') }
+ let(:invited_group) { create(:group) { |group| group.add_developer(invited_group_developer) } }
let(:project) do
create(:project, :public, group: nested_group) do |project|
@@ -146,19 +148,21 @@ RSpec.describe API::Members, feature_category: :subgroups do
let(:nested_group) do
create(:group, parent: group) do |nested_group|
nested_group.add_developer(nested_user)
+ create(:group_group_link, :guest, shared_with_group: invited_group, shared_group: nested_group)
end
end
- it 'finds all project members including inherited members' do
+ it 'finds all project members including inherited members and members shared into ancestor groups' do
get api("/projects/#{project.id}/members/all", developer)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.map { |u| u['id'] }).to match_array [maintainer.id, developer.id, nested_user.id, project_user.id, linked_group_user.id]
+ expected_user_ids = [maintainer.id, developer.id, nested_user.id, project_user.id, linked_group_user.id, invited_group_developer.id]
+ expect(json_response.map { |u| u['id'] }).to match_array expected_user_ids
end
- it 'returns only one member for each user without returning duplicated members' do
+ it 'returns only one member for each user without returning duplicated members with correct access levels' do
linked_group.add_developer(developer)
get api("/projects/#{project.id}/members/all", developer)
@@ -172,7 +176,8 @@ RSpec.describe API::Members, feature_category: :subgroups do
[maintainer.id, Gitlab::Access::OWNER],
[nested_user.id, Gitlab::Access::DEVELOPER],
[project_user.id, Gitlab::Access::DEVELOPER],
- [linked_group_user.id, Gitlab::Access::DEVELOPER]
+ [linked_group_user.id, Gitlab::Access::DEVELOPER],
+ [invited_group_developer.id, Gitlab::Access::GUEST]
]
expect(json_response.map { |u| [u['id'], u['access_level']] }).to match_array(expected_users_and_access_levels)
end
@@ -183,7 +188,8 @@ RSpec.describe API::Members, feature_category: :subgroups do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.map { |u| u['id'] }).to match_array [maintainer.id, developer.id, nested_user.id]
+ expected_user_ids = [maintainer.id, developer.id, nested_user.id, invited_group_developer.id]
+ expect(json_response.map { |u| u['id'] }).to match_array expected_user_ids
end
context 'with a subgroup' do
@@ -739,6 +745,30 @@ RSpec.describe API::Members, feature_category: :subgroups do
end.to change { source.members.count }.by(-1)
end
+ it_behaves_like 'rate limited endpoint', rate_limit_key: :member_delete do
+ let(:current_user) { maintainer }
+
+ let(:another_member) { create(:user) }
+
+ before do
+ source.add_developer(another_member)
+ end
+
+ # We rate limit scoped by the group / project
+ let(:delete_paths) do
+ [
+ api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", maintainer),
+ api("/#{source_type.pluralize}/#{source.id}/members/#{another_member.id}", maintainer)
+ ]
+ end
+
+ def request
+ delete_member_path = delete_paths.shift
+
+ delete delete_member_path
+ end
+ end
+
it_behaves_like '412 response' do
let(:request) { api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", maintainer) }
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 19a630e5218..50e70a9dc0f 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe API::MergeRequests, feature_category: :source_code_management do
+RSpec.describe API::MergeRequests, :aggregate_failures, feature_category: :source_code_management do
include ProjectForksHelper
let_it_be(:base_time) { Time.now }
@@ -50,6 +50,27 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do
expect_successful_response_with_paginated_array
end
+ context 'when merge request is unchecked' do
+ let(:check_service_class) { MergeRequests::MergeabilityCheckService }
+ let(:mr_entity) { json_response.find { |mr| mr['id'] == merge_request.id } }
+ let(:merge_request) { create(:merge_request, :simple, author: user, source_project: project, title: "Test") }
+
+ before do
+ merge_request.mark_as_unchecked!
+ end
+
+ context 'with merge status recheck projection' do
+ it 'does not enqueue a merge status recheck' do
+ expect(check_service_class).not_to receive(:new)
+
+ get(api(endpoint_path), params: { with_merge_status_recheck: true })
+
+ expect_successful_response_with_paginated_array
+ expect(mr_entity['merge_status']).to eq('unchecked')
+ end
+ end
+ end
+
it_behaves_like 'issuable API rate-limited search' do
let(:url) { endpoint_path }
let(:issuable) { merge_request }
@@ -85,28 +106,67 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do
merge_request.mark_as_unchecked!
end
- context 'with merge status recheck projection' do
- it 'checks mergeability asynchronously' do
- expect_next_instances_of(check_service_class, (1..2)) do |service|
- expect(service).not_to receive(:execute)
- expect(service).to receive(:async_execute).and_call_original
+ context 'with a developer+ role' do
+ before do
+ project.add_developer(user2)
+ end
+
+ context 'with merge status recheck projection' do
+ it 'checks mergeability asynchronously' do
+ expect_next_instances_of(check_service_class, (1..2)) do |service|
+ expect(service).not_to receive(:execute)
+ expect(service).to receive(:async_execute).and_call_original
+ end
+
+ get(api(endpoint_path, user2), params: { with_merge_status_recheck: true })
+
+ expect_successful_response_with_paginated_array
+ expect(mr_entity['merge_status']).to eq('checking')
end
+ end
- get(api(endpoint_path, user), params: { with_merge_status_recheck: true })
+ context 'without merge status recheck projection' do
+ it 'does not enqueue a merge status recheck' do
+ expect(check_service_class).not_to receive(:new)
- expect_successful_response_with_paginated_array
- expect(mr_entity['merge_status']).to eq('checking')
+ get api(endpoint_path, user2)
+
+ expect_successful_response_with_paginated_array
+ expect(mr_entity['merge_status']).to eq('unchecked')
+ end
end
end
- context 'without merge status recheck projection' do
- it 'does not enqueue a merge status recheck' do
- expect(check_service_class).not_to receive(:new)
+ context 'with a reporter role' do
+ context 'with merge status recheck projection' do
+ it 'does not enqueue a merge status recheck' do
+ expect(check_service_class).not_to receive(:new)
- get api(endpoint_path, user)
+ get(api(endpoint_path, user2), params: { with_merge_status_recheck: true })
- expect_successful_response_with_paginated_array
- expect(mr_entity['merge_status']).to eq('unchecked')
+ expect_successful_response_with_paginated_array
+ expect(mr_entity['merge_status']).to eq('unchecked')
+ end
+ end
+
+ context 'when restrict_merge_status_recheck FF is disabled' do
+ before do
+ stub_feature_flags(restrict_merge_status_recheck: false)
+ end
+
+ context 'with merge status recheck projection' do
+ it 'does enqueue a merge status recheck' do
+ expect_next_instances_of(check_service_class, (1..2)) do |service|
+ expect(service).not_to receive(:execute)
+ expect(service).to receive(:async_execute).and_call_original
+ end
+
+ get(api(endpoint_path, user2), params: { with_merge_status_recheck: true })
+
+ expect_successful_response_with_paginated_array
+ expect(mr_entity['merge_status']).to eq('checking')
+ end
+ end
end
end
end
@@ -168,6 +228,17 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do
end
end
+ context 'when DB timeouts occur' do
+ it 'returns a :request_timeout status' do
+ allow(MergeRequestsFinder).to receive(:new).and_raise(ActiveRecord::QueryCanceled)
+
+ path = endpoint_path + '?view=simple'
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(:request_timeout)
+ end
+ end
+
it 'returns an array of all merge_requests using simple mode' do
path = endpoint_path + '?view=simple'
@@ -238,6 +309,35 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do
expect(response).to match_response_schema('public_api/v4/merge_requests')
end
+ context 'with approved param' do
+ let(:approved_mr) { create(:merge_request, target_project: project, source_project: project) }
+
+ before do
+ create(:approval, merge_request: approved_mr)
+ end
+
+ it 'returns only approved merge requests' do
+ path = endpoint_path + '?approved=yes'
+
+ get api(path, user)
+
+ expect_paginated_array_response([approved_mr.id])
+ end
+
+ it 'returns only non-approved merge requests' do
+ path = endpoint_path + '?approved=no'
+
+ get api(path, user)
+
+ expect_paginated_array_response([
+ merge_request_merged.id,
+ merge_request_locked.id,
+ merge_request_closed.id,
+ merge_request.id
+ ])
+ end
+ end
+
it 'returns an empty array if no issue matches milestone' do
get api(endpoint_path, user), params: { milestone: '1.0.0' }
@@ -483,7 +583,7 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do
create(:label_link, label: label2, target: merge_request2)
end
- it 'returns merge requests without any of the labels given', :aggregate_failures do
+ it 'returns merge requests without any of the labels given' do
get api(endpoint_path, user), params: { not: { labels: ["#{label.title}, #{label2.title}"] } }
expect(response).to have_gitlab_http_status(:ok)
@@ -494,7 +594,7 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do
end
end
- it 'returns merge requests without any of the milestones given', :aggregate_failures do
+ it 'returns merge requests without any of the milestones given' do
get api(endpoint_path, user), params: { not: { milestone: milestone.title } }
expect(response).to have_gitlab_http_status(:ok)
@@ -505,7 +605,7 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do
end
end
- it 'returns merge requests without the author given', :aggregate_failures do
+ it 'returns merge requests without the author given' do
get api(endpoint_path, user), params: { not: { author_id: user2.id } }
expect(response).to have_gitlab_http_status(:ok)
@@ -516,7 +616,7 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do
end
end
- it 'returns merge requests without the assignee given', :aggregate_failures do
+ it 'returns merge requests without the assignee given' do
get api(endpoint_path, user), params: { not: { assignee_id: user2.id } }
expect(response).to have_gitlab_http_status(:ok)
@@ -1326,7 +1426,7 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do
expect(json_response['merge_error']).to eq(merge_request.merge_error)
expect(json_response['user']['can_merge']).to be_truthy
expect(json_response).not_to include('rebase_in_progress')
- expect(json_response['first_contribution']).to be false
+ expect(json_response['first_contribution']).to be true
expect(json_response['has_conflicts']).to be false
expect(json_response['blocking_discussions_resolved']).to be_truthy
expect(json_response['references']['short']).to eq("!#{merge_request.iid}")
@@ -3437,8 +3537,13 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do
end
describe 'POST :id/merge_requests/:merge_request_iid/subscribe' do
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:path) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe" }
+ let(:params) { {} }
+ end
+
it 'subscribes to a merge request' do
- post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", admin)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:created)
expect(json_response['subscribed']).to eq(true)
@@ -3481,7 +3586,7 @@ RSpec.describe API::MergeRequests, feature_category: :source_code_management do
end
it 'returns 304 if not subscribed' do
- post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", admin)
+ post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_modified)
end
diff --git a/spec/requests/api/metadata_spec.rb b/spec/requests/api/metadata_spec.rb
index b9bdadb01cc..e15186c48a5 100644
--- a/spec/requests/api/metadata_spec.rb
+++ b/spec/requests/api/metadata_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Metadata, feature_category: :not_owned do
+RSpec.describe API::Metadata, feature_category: :shared do
shared_examples_for 'GET /metadata' do
context 'when unauthenticated' do
it 'returns authentication error' do
diff --git a/spec/requests/api/metrics/dashboard/annotations_spec.rb b/spec/requests/api/metrics/dashboard/annotations_spec.rb
index 7932dd29e4d..250fe2a3ee3 100644
--- a/spec/requests/api/metrics/dashboard/annotations_spec.rb
+++ b/spec/requests/api/metrics/dashboard/annotations_spec.rb
@@ -16,6 +16,7 @@ RSpec.describe API::Metrics::Dashboard::Annotations, feature_category: :metrics
let(:url) { "/#{source_type.pluralize}/#{source.id}/metrics_dashboard/annotations" }
before do
+ stub_feature_flags(remove_monitor_metrics: false)
project.add_developer(user)
end
@@ -35,7 +36,7 @@ RSpec.describe API::Metrics::Dashboard::Annotations, feature_category: :metrics
end
context 'with invalid parameters' do
- it 'returns error messsage' do
+ it 'returns error message' do
post api(url, user), params: { dashboard_path: '', starting_at: nil, description: nil }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -104,6 +105,18 @@ RSpec.describe API::Metrics::Dashboard::Annotations, feature_category: :metrics
expect(response).to have_gitlab_http_status(:forbidden)
end
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'returns 404 not found' do
+ post api(url, user), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
end
diff --git a/spec/requests/api/metrics/user_starred_dashboards_spec.rb b/spec/requests/api/metrics/user_starred_dashboards_spec.rb
index 38d3c0be8b2..6fc98de0777 100644
--- a/spec/requests/api/metrics/user_starred_dashboards_spec.rb
+++ b/spec/requests/api/metrics/user_starred_dashboards_spec.rb
@@ -15,6 +15,10 @@ RSpec.describe API::Metrics::UserStarredDashboards, feature_category: :metrics d
}
end
+ before do
+ stub_feature_flags(remove_monitor_metrics: false)
+ end
+
describe 'POST /projects/:id/metrics/user_starred_dashboards' do
before do
project.add_reporter(user)
@@ -84,6 +88,18 @@ RSpec.describe API::Metrics::UserStarredDashboards, feature_category: :metrics d
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'returns 404 not found' do
+ post api(url, user), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
describe 'DELETE /projects/:id/metrics/user_starred_dashboards' do
@@ -161,5 +177,17 @@ RSpec.describe API::Metrics::UserStarredDashboards, feature_category: :metrics d
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'returns 404 not found' do
+ delete api(url, user), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
end
diff --git a/spec/requests/api/ml/mlflow/experiments_spec.rb b/spec/requests/api/ml/mlflow/experiments_spec.rb
new file mode 100644
index 00000000000..1a2577e69e7
--- /dev/null
+++ b/spec/requests/api/ml/mlflow/experiments_spec.rb
@@ -0,0 +1,215 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Ml::Mlflow::Experiments, feature_category: :mlops do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:another_project) { build(:project).tap { |p| p.add_developer(developer) } }
+ let_it_be(:experiment) do
+ create(:ml_experiments, :with_metadata, project: project)
+ end
+
+ let_it_be(:tokens) do
+ {
+ write: create(:personal_access_token, scopes: %w[read_api api], user: developer),
+ read: create(:personal_access_token, scopes: %w[read_api], user: developer),
+ no_access: create(:personal_access_token, scopes: %w[read_user], user: developer),
+ different_user: create(:personal_access_token, scopes: %w[read_api api], user: build(:user))
+ }
+ end
+
+ let(:current_user) { developer }
+ let(:ff_value) { true }
+ let(:access_token) { tokens[:write] }
+ let(:headers) { { 'Authorization' => "Bearer #{access_token.token}" } }
+ let(:project_id) { project.id }
+ let(:default_params) { {} }
+ let(:params) { default_params }
+ let(:request) { get api(route), params: params, headers: headers }
+ let(:json_response) { Gitlab::Json.parse(api_response.body) }
+ let(:presented_experiment) do
+ {
+ 'experiment_id' => experiment.iid.to_s,
+ 'name' => experiment.name,
+ 'lifecycle_stage' => 'active',
+ 'artifact_location' => 'not_implemented',
+ 'tags' => [
+ {
+ 'key' => experiment.metadata[0].name,
+ 'value' => experiment.metadata[0].value
+ },
+ {
+ 'key' => experiment.metadata[1].name,
+ 'value' => experiment.metadata[1].value
+ }
+ ]
+ }
+ end
+
+ subject(:api_response) do
+ request
+ response
+ end
+
+ before do
+ stub_feature_flags(ml_experiment_tracking: ff_value)
+ end
+
+ describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/get' do
+ let(:experiment_iid) { experiment.iid.to_s }
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/get?experiment_id=#{experiment_iid}" }
+
+ it 'returns the experiment', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ is_expected.to match_response_schema('ml/get_experiment')
+ expect(json_response).to include({ 'experiment' => presented_experiment })
+ end
+
+ describe 'Error States' do
+ context 'when has access' do
+ context 'and experiment does not exist' do
+ let(:experiment_iid) { non_existing_record_iid.to_s }
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+
+ context 'and experiment_id is not passed' do
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/get" }
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+ end
+
+ it_behaves_like 'MLflow|shared error cases'
+ it_behaves_like 'MLflow|Requires read_api scope'
+ end
+ end
+
+ describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/list' do
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/list" }
+
+ it 'returns the experiments', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ is_expected.to match_response_schema('ml/list_experiments')
+ expect(json_response).to include({ 'experiments' => [presented_experiment] })
+ end
+
+ context 'when there are no experiments' do
+ let(:project_id) { another_project.id }
+
+ it 'returns an empty list' do
+ expect(json_response).to include({ 'experiments' => [] })
+ end
+ end
+
+ describe 'Error States' do
+ it_behaves_like 'MLflow|shared error cases'
+ it_behaves_like 'MLflow|Requires read_api scope'
+ end
+ end
+
+ describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/get-by-name' do
+ let(:experiment_name) { experiment.name }
+ let(:route) do
+ "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/get-by-name?experiment_name=#{experiment_name}"
+ end
+
+ it 'returns the experiment', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ is_expected.to match_response_schema('ml/get_experiment')
+ expect(json_response).to include({ 'experiment' => presented_experiment })
+ end
+
+ describe 'Error States' do
+ context 'when has access but experiment does not exist' do
+ let(:experiment_name) { "random_experiment" }
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+
+ context 'when has access but experiment_name is not passed' do
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/get-by-name" }
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+
+ it_behaves_like 'MLflow|shared error cases'
+ it_behaves_like 'MLflow|Requires read_api scope'
+ end
+ end
+
+ describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/create' do
+ let(:route) do
+ "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/create"
+ end
+
+ let(:params) { { name: 'new_experiment' } }
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'creates the experiment', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ expect(json_response).to include('experiment_id')
+ end
+
+ describe 'Error States' do
+ context 'when experiment name is not passed' do
+ let(:params) { {} }
+
+ it_behaves_like 'MLflow|Bad Request'
+ end
+
+ context 'when experiment name already exists' do
+ let(:existing_experiment) do
+ create(:ml_experiments, user: current_user, project: project)
+ end
+
+ let(:params) { { name: existing_experiment.name } }
+
+ it "is Bad Request", :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:bad_request)
+
+ expect(json_response).to include({ 'error_code' => 'RESOURCE_ALREADY_EXISTS' })
+ end
+ end
+
+ context 'when project does not exist' do
+ let(:route) { "/projects/#{non_existing_record_id}/ml/mlflow/api/2.0/mlflow/experiments/create" }
+
+ it "is Not Found", :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:not_found)
+
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+ end
+
+ it_behaves_like 'MLflow|shared error cases'
+ it_behaves_like 'MLflow|Requires api scope'
+ end
+ end
+
+ describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/set-experiment-tag' do
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/set-experiment-tag" }
+ let(:default_params) { { experiment_id: experiment.iid.to_s, key: 'some_key', value: 'value' } }
+ let(:params) { default_params }
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'logs the tag', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ expect(json_response).to be_empty
+ expect(experiment.reload.metadata.map(&:name)).to include('some_key')
+ end
+
+ describe 'Error Cases' do
+ context 'when tag was already set' do
+ let(:params) { default_params.merge(key: experiment.metadata[0].name) }
+
+ it_behaves_like 'MLflow|Bad Request'
+ end
+
+ it_behaves_like 'MLflow|shared error cases'
+ it_behaves_like 'MLflow|Requires api scope'
+ it_behaves_like 'MLflow|Bad Request on missing required', [:key, :value]
+ end
+ end
+end
diff --git a/spec/requests/api/ml/mlflow/runs_spec.rb b/spec/requests/api/ml/mlflow/runs_spec.rb
new file mode 100644
index 00000000000..746372b7978
--- /dev/null
+++ b/spec/requests/api/ml/mlflow/runs_spec.rb
@@ -0,0 +1,354 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Ml::Mlflow::Runs, feature_category: :mlops do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:another_project) { build(:project).tap { |p| p.add_developer(developer) } }
+ let_it_be(:experiment) do
+ create(:ml_experiments, :with_metadata, project: project)
+ end
+
+ let_it_be(:candidate) do
+ create(:ml_candidates,
+ :with_metrics_and_params, :with_metadata,
+ user: experiment.user, start_time: 1234, experiment: experiment, project: project)
+ end
+
+ let_it_be(:tokens) do
+ {
+ write: create(:personal_access_token, scopes: %w[read_api api], user: developer),
+ read: create(:personal_access_token, scopes: %w[read_api], user: developer),
+ no_access: create(:personal_access_token, scopes: %w[read_user], user: developer),
+ different_user: create(:personal_access_token, scopes: %w[read_api api], user: build(:user))
+ }
+ end
+
+ let(:current_user) { developer }
+ let(:ff_value) { true }
+ let(:access_token) { tokens[:write] }
+ let(:headers) { { 'Authorization' => "Bearer #{access_token.token}" } }
+ let(:project_id) { project.id }
+ let(:default_params) { {} }
+ let(:params) { default_params }
+ let(:request) { get api(route), params: params, headers: headers }
+ let(:json_response) { Gitlab::Json.parse(api_response.body) }
+
+ subject(:api_response) do
+ request
+ response
+ end
+
+ before do
+ stub_feature_flags(ml_experiment_tracking: ff_value)
+ end
+
+ RSpec.shared_examples 'MLflow|run_id param error cases' do
+ context 'when run id is not passed' do
+ let(:params) { {} }
+
+ it "is Bad Request" do
+ is_expected.to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'when run_id is invalid' do
+ let(:params) { default_params.merge(run_id: non_existing_record_iid.to_s) }
+
+ it "is Resource Does Not Exist", :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:not_found)
+
+ expect(json_response).to include({ "error_code" => 'RESOURCE_DOES_NOT_EXIST' })
+ end
+ end
+
+ context 'when run_id is not in in the project' do
+ let(:project_id) { another_project.id }
+
+ it "is Resource Does Not Exist", :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:not_found)
+
+ expect(json_response).to include({ "error_code" => 'RESOURCE_DOES_NOT_EXIST' })
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/create' do
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/create" }
+ let(:params) do
+ {
+ experiment_id: experiment.iid.to_s,
+ start_time: Time.now.to_i,
+ run_name: "A new Run",
+ tags: [
+ { key: 'hello', value: 'world' }
+ ]
+ }
+ end
+
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'creates the run', :aggregate_failures do
+ expected_properties = {
+ 'experiment_id' => params[:experiment_id],
+ 'user_id' => current_user.id.to_s,
+ 'run_name' => "A new Run",
+ 'start_time' => params[:start_time],
+ 'status' => 'RUNNING',
+ 'lifecycle_stage' => 'active'
+ }
+
+ is_expected.to have_gitlab_http_status(:ok)
+ is_expected.to match_response_schema('ml/run')
+ expect(json_response['run']).to include('info' => hash_including(**expected_properties),
+ 'data' => {
+ 'metrics' => [],
+ 'params' => [],
+ 'tags' => [{ 'key' => 'hello', 'value' => 'world' }]
+ })
+ end
+
+ describe 'Error States' do
+ context 'when experiment id is not passed' do
+ let(:params) { {} }
+
+ it_behaves_like 'MLflow|Bad Request'
+ end
+
+ context 'when experiment id does not exist' do
+ let(:params) { { experiment_id: non_existing_record_iid.to_s } }
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+
+ context 'when experiment exists but is not part of the project' do
+ let(:project_id) { another_project.id }
+
+ it_behaves_like 'MLflow|Not Found - Resource Does Not Exist'
+ end
+
+ it_behaves_like 'MLflow|shared error cases'
+ it_behaves_like 'MLflow|Requires api scope'
+ end
+ end
+
+ describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/runs/get' do
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/get" }
+ let(:default_params) { { 'run_id' => candidate.eid } }
+
+ it 'gets the run', :aggregate_failures do
+ expected_properties = {
+ 'experiment_id' => candidate.experiment.iid.to_s,
+ 'user_id' => candidate.user.id.to_s,
+ 'start_time' => candidate.start_time,
+ 'artifact_uri' => "http://www.example.com/api/v4/projects/#{project_id}/packages/generic/ml_experiment_#{experiment.iid}/#{candidate.iid}/",
+ 'status' => "RUNNING",
+ 'lifecycle_stage' => "active"
+ }
+
+ is_expected.to have_gitlab_http_status(:ok)
+ is_expected.to match_response_schema('ml/run')
+ expect(json_response['run']).to include(
+ 'info' => hash_including(**expected_properties),
+ 'data' => {
+ 'metrics' => [
+ hash_including('key' => candidate.metrics[0].name),
+ hash_including('key' => candidate.metrics[1].name)
+ ],
+ 'params' => [
+ { 'key' => candidate.params[0].name, 'value' => candidate.params[0].value },
+ { 'key' => candidate.params[1].name, 'value' => candidate.params[1].value }
+ ],
+ 'tags' => [
+ { 'key' => candidate.metadata[0].name, 'value' => candidate.metadata[0].value },
+ { 'key' => candidate.metadata[1].name, 'value' => candidate.metadata[1].value }
+ ]
+ })
+ end
+
+ describe 'Error States' do
+ it_behaves_like 'MLflow|run_id param error cases'
+ it_behaves_like 'MLflow|shared error cases'
+ it_behaves_like 'MLflow|Requires read_api scope'
+ end
+ end
+
+ describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/update' do
+ let(:default_params) { { run_id: candidate.eid.to_s, status: 'FAILED', end_time: Time.now.to_i } }
+ let(:request) { post api(route), params: params, headers: headers }
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/update" }
+
+ it 'updates the run', :aggregate_failures do
+ expected_properties = {
+ 'experiment_id' => candidate.experiment.iid.to_s,
+ 'user_id' => candidate.user.id.to_s,
+ 'start_time' => candidate.start_time,
+ 'end_time' => params[:end_time],
+ 'artifact_uri' => "http://www.example.com/api/v4/projects/#{project_id}/packages/generic/ml_experiment_#{experiment.iid}/#{candidate.iid}/",
+ 'status' => 'FAILED',
+ 'lifecycle_stage' => 'active'
+ }
+
+ is_expected.to have_gitlab_http_status(:ok)
+ is_expected.to match_response_schema('ml/update_run')
+ expect(json_response).to include('run_info' => hash_including(**expected_properties))
+ end
+
+ describe 'Error States' do
+ context 'when status in invalid' do
+ let(:params) { default_params.merge(status: 'YOLO') }
+
+ it_behaves_like 'MLflow|Bad Request'
+ end
+
+ context 'when end_time is invalid' do
+ let(:params) { default_params.merge(end_time: 's') }
+
+ it_behaves_like 'MLflow|Bad Request'
+ end
+
+ it_behaves_like 'MLflow|shared error cases'
+ it_behaves_like 'MLflow|Requires api scope'
+ it_behaves_like 'MLflow|run_id param error cases'
+ end
+ end
+
+ describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/log-metric' do
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/log-metric" }
+ let(:default_params) { { run_id: candidate.eid.to_s, key: 'some_key', value: 10.0, timestamp: Time.now.to_i } }
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'logs the metric', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ expect(json_response).to be_empty
+ expect(candidate.metrics.reload.length).to eq(3)
+ end
+
+ describe 'Error Cases' do
+ it_behaves_like 'MLflow|shared error cases'
+ it_behaves_like 'MLflow|Requires api scope'
+ it_behaves_like 'MLflow|run_id param error cases'
+ it_behaves_like 'MLflow|Bad Request on missing required', [:key, :value, :timestamp]
+ end
+ end
+
+ describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/log-parameter' do
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/log-parameter" }
+ let(:default_params) { { run_id: candidate.eid.to_s, key: 'some_key', value: 'value' } }
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'logs the parameter', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ expect(json_response).to be_empty
+ expect(candidate.params.reload.length).to eq(3)
+ end
+
+ describe 'Error Cases' do
+ context 'when parameter was already logged' do
+ let(:params) { default_params.tap { |p| p[:key] = candidate.params[0].name } }
+
+ it_behaves_like 'MLflow|Bad Request'
+ end
+
+ it_behaves_like 'MLflow|shared error cases'
+ it_behaves_like 'MLflow|Requires api scope'
+ it_behaves_like 'MLflow|run_id param error cases'
+ it_behaves_like 'MLflow|Bad Request on missing required', [:key, :value]
+ end
+ end
+
+ describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/set-tag' do
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/set-tag" }
+ let(:default_params) { { run_id: candidate.eid.to_s, key: 'some_key', value: 'value' } }
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'logs the tag', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ expect(json_response).to be_empty
+ expect(candidate.reload.metadata.map(&:name)).to include('some_key')
+ end
+
+ describe 'Error Cases' do
+ context 'when tag was already logged' do
+ let(:params) { default_params.tap { |p| p[:key] = candidate.metadata[0].name } }
+
+ it_behaves_like 'MLflow|Bad Request'
+ end
+
+ it_behaves_like 'MLflow|shared error cases'
+ it_behaves_like 'MLflow|Requires api scope'
+ it_behaves_like 'MLflow|run_id param error cases'
+ it_behaves_like 'MLflow|Bad Request on missing required', [:key, :value]
+ end
+ end
+
+ describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/log-batch' do
+ let_it_be(:candidate2) do
+ create(:ml_candidates, user: experiment.user, start_time: 1234, experiment: experiment, project: project)
+ end
+
+ let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/log-batch" }
+ let(:default_params) do
+ {
+ run_id: candidate2.eid.to_s,
+ metrics: [
+ { key: 'mae', value: 2.5, timestamp: 1552550804 },
+ { key: 'rmse', value: 2.7, timestamp: 1552550804 }
+ ],
+ params: [{ key: 'model_class', value: 'LogisticRegression' }],
+ tags: [{ key: 'tag1', value: 'tag.value.1' }]
+ }
+ end
+
+ let(:request) { post api(route), params: params, headers: headers }
+
+ it 'logs parameters and metrics', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ expect(json_response).to be_empty
+ expect(candidate2.params.size).to eq(1)
+ expect(candidate2.metadata.size).to eq(1)
+ expect(candidate2.metrics.size).to eq(2)
+ end
+
+ context 'when parameter was already logged' do
+ let(:params) do
+ default_params.tap { |p| p[:params] = [{ key: 'hello', value: 'a' }, { key: 'hello', value: 'b' }] }
+ end
+
+ it 'does not log', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ expect(candidate2.params.reload.size).to eq(1)
+ end
+ end
+
+ context 'when tag was already logged' do
+ let(:params) do
+ default_params.tap { |p| p[:tags] = [{ key: 'tag1', value: 'a' }, { key: 'tag1', value: 'b' }] }
+ end
+
+ it 'logs only 1', :aggregate_failures do
+ is_expected.to have_gitlab_http_status(:ok)
+ expect(candidate2.metadata.reload.size).to eq(1)
+ end
+ end
+
+ describe 'Error Cases' do
+ context 'when required metric key is missing' do
+ let(:params) { default_params.tap { |p| p[:metrics] = [p[:metrics][0].delete(:key)] } }
+
+ it_behaves_like 'MLflow|Bad Request'
+ end
+
+ context 'when required param key is missing' do
+ let(:params) { default_params.tap { |p| p[:params] = [p[:params][0].delete(:key)] } }
+
+ it_behaves_like 'MLflow|Bad Request'
+ end
+
+ it_behaves_like 'MLflow|shared error cases'
+ it_behaves_like 'MLflow|Requires api scope'
+ it_behaves_like 'MLflow|run_id param error cases'
+ end
+ end
+end
diff --git a/spec/requests/api/ml/mlflow_spec.rb b/spec/requests/api/ml/mlflow_spec.rb
deleted file mode 100644
index fdf115f7e92..00000000000
--- a/spec/requests/api/ml/mlflow_spec.rb
+++ /dev/null
@@ -1,630 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require 'mime/types'
-
-RSpec.describe API::Ml::Mlflow, feature_category: :mlops do
- include SessionHelpers
- include ApiHelpers
- include HttpBasicAuthHelpers
-
- let_it_be(:project) { create(:project, :private) }
- let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
- let_it_be(:another_project) { build(:project).tap { |p| p.add_developer(developer) } }
- let_it_be(:experiment) do
- create(:ml_experiments, :with_metadata, project: project)
- end
-
- let_it_be(:candidate) do
- create(:ml_candidates,
- :with_metrics_and_params, :with_metadata,
- user: experiment.user, start_time: 1234, experiment: experiment)
- end
-
- let_it_be(:tokens) do
- {
- write: create(:personal_access_token, scopes: %w[read_api api], user: developer),
- read: create(:personal_access_token, scopes: %w[read_api], user: developer),
- no_access: create(:personal_access_token, scopes: %w[read_user], user: developer),
- different_user: create(:personal_access_token, scopes: %w[read_api api], user: build(:user))
- }
- end
-
- let(:current_user) { developer }
- let(:ff_value) { true }
- let(:access_token) { tokens[:write] }
- let(:headers) do
- { 'Authorization' => "Bearer #{access_token.token}" }
- end
-
- let(:project_id) { project.id }
- let(:default_params) { {} }
- let(:params) { default_params }
- let(:request) { get api(route), params: params, headers: headers }
-
- before do
- stub_feature_flags(ml_experiment_tracking: ff_value)
-
- request
- end
-
- shared_examples 'Not Found' do |message|
- it "is Not Found" do
- expect(response).to have_gitlab_http_status(:not_found)
-
- expect(json_response['message']).to eq(message) if message.present?
- end
- end
-
- shared_examples 'Not Found - Resource Does Not Exist' do
- it "is Resource Does Not Exist" do
- expect(response).to have_gitlab_http_status(:not_found)
-
- expect(json_response).to include({ "error_code" => 'RESOURCE_DOES_NOT_EXIST' })
- end
- end
-
- shared_examples 'Requires api scope' do
- context 'when user has access but token has wrong scope' do
- let(:access_token) { tokens[:read] }
-
- it { expect(response).to have_gitlab_http_status(:forbidden) }
- end
- end
-
- shared_examples 'Requires read_api scope' do
- context 'when user has access but token has wrong scope' do
- let(:access_token) { tokens[:no_access] }
-
- it { expect(response).to have_gitlab_http_status(:forbidden) }
- end
- end
-
- shared_examples 'Bad Request' do |error_code = nil|
- it "is Bad Request" do
- expect(response).to have_gitlab_http_status(:bad_request)
-
- expect(json_response).to include({ 'error_code' => error_code }) if error_code.present?
- end
- end
-
- shared_examples 'shared error cases' do
- context 'when not authenticated' do
- let(:headers) { {} }
-
- it "is Unauthorized" do
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
-
- context 'when user does not have access' do
- let(:access_token) { tokens[:different_user] }
-
- it_behaves_like 'Not Found'
- end
-
- context 'when ff is disabled' do
- let(:ff_value) { false }
-
- it_behaves_like 'Not Found'
- end
- end
-
- shared_examples 'run_id param error cases' do
- context 'when run id is not passed' do
- let(:params) { {} }
-
- it_behaves_like 'Bad Request'
- end
-
- context 'when run_id is invalid' do
- let(:params) { default_params.merge(run_id: non_existing_record_iid.to_s) }
-
- it_behaves_like 'Not Found - Resource Does Not Exist'
- end
-
- context 'when run_id is not in in the project' do
- let(:project_id) { another_project.id }
-
- it_behaves_like 'Not Found - Resource Does Not Exist'
- end
- end
-
- shared_examples 'Bad Request on missing required' do |keys|
- keys.each do |key|
- context "when \"#{key}\" is missing" do
- let(:params) { default_params.tap { |p| p.delete(key) } }
-
- it_behaves_like 'Bad Request'
- end
- end
- end
-
- describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/get' do
- let(:experiment_iid) { experiment.iid.to_s }
- let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/get?experiment_id=#{experiment_iid}" }
-
- it 'returns the experiment', :aggregate_failures do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('ml/get_experiment')
- expect(json_response).to include({
- 'experiment' => {
- 'experiment_id' => experiment_iid,
- 'name' => experiment.name,
- 'lifecycle_stage' => 'active',
- 'artifact_location' => 'not_implemented',
- 'tags' => [
- {
- 'key' => experiment.metadata[0].name,
- 'value' => experiment.metadata[0].value
- },
- {
- 'key' => experiment.metadata[1].name,
- 'value' => experiment.metadata[1].value
- }
- ]
- }
- })
- end
-
- describe 'Error States' do
- context 'when has access' do
- context 'and experiment does not exist' do
- let(:experiment_iid) { non_existing_record_iid.to_s }
-
- it_behaves_like 'Not Found - Resource Does Not Exist'
- end
-
- context 'and experiment_id is not passed' do
- let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/get" }
-
- it_behaves_like 'Not Found - Resource Does Not Exist'
- end
- end
-
- it_behaves_like 'shared error cases'
- it_behaves_like 'Requires read_api scope'
- end
- end
-
- describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/list' do
- let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/list" }
-
- it 'returns the experiments' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('ml/list_experiments')
- expect(json_response).to include({
- 'experiments' => [
- 'experiment_id' => experiment.iid.to_s,
- 'name' => experiment.name,
- 'lifecycle_stage' => 'active',
- 'artifact_location' => 'not_implemented',
- 'tags' => [
- {
- 'key' => experiment.metadata[0].name,
- 'value' => experiment.metadata[0].value
- },
- {
- 'key' => experiment.metadata[1].name,
- 'value' => experiment.metadata[1].value
- }
- ]
- ]
- })
- end
-
- context 'when there are no experiments' do
- let(:project_id) { another_project.id }
-
- it 'returns an empty list' do
- expect(json_response).to include({ 'experiments' => [] })
- end
- end
-
- describe 'Error States' do
- it_behaves_like 'shared error cases'
- it_behaves_like 'Requires read_api scope'
- end
- end
-
- describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/get-by-name' do
- let(:experiment_name) { experiment.name }
- let(:route) do
- "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/get-by-name?experiment_name=#{experiment_name}"
- end
-
- it 'returns the experiment', :aggregate_failures do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('ml/get_experiment')
- expect(json_response).to include({
- 'experiment' => {
- 'experiment_id' => experiment.iid.to_s,
- 'name' => experiment_name,
- 'lifecycle_stage' => 'active',
- 'artifact_location' => 'not_implemented',
- 'tags' => [
- {
- 'key' => experiment.metadata[0].name,
- 'value' => experiment.metadata[0].value
- },
- {
- 'key' => experiment.metadata[1].name,
- 'value' => experiment.metadata[1].value
- }
- ]
- }
- })
- end
-
- describe 'Error States' do
- context 'when has access but experiment does not exist' do
- let(:experiment_name) { "random_experiment" }
-
- it_behaves_like 'Not Found - Resource Does Not Exist'
- end
-
- context 'when has access but experiment_name is not passed' do
- let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/get-by-name" }
-
- it_behaves_like 'Not Found - Resource Does Not Exist'
- end
-
- it_behaves_like 'shared error cases'
- it_behaves_like 'Requires read_api scope'
- end
- end
-
- describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/create' do
- let(:route) do
- "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/create"
- end
-
- let(:params) { { name: 'new_experiment' } }
- let(:request) { post api(route), params: params, headers: headers }
-
- it 'creates the experiment', :aggregate_failures do
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to include('experiment_id')
- end
-
- describe 'Error States' do
- context 'when experiment name is not passed' do
- let(:params) { {} }
-
- it_behaves_like 'Bad Request'
- end
-
- context 'when experiment name already exists' do
- let(:existing_experiment) do
- create(:ml_experiments, user: current_user, project: project)
- end
-
- let(:params) { { name: existing_experiment.name } }
-
- it_behaves_like 'Bad Request', 'RESOURCE_ALREADY_EXISTS'
- end
-
- context 'when project does not exist' do
- let(:route) { "/projects/#{non_existing_record_id}/ml/mlflow/api/2.0/mlflow/experiments/create" }
-
- it_behaves_like 'Not Found', '404 Project Not Found'
- end
-
- it_behaves_like 'shared error cases'
- it_behaves_like 'Requires api scope'
- end
- end
-
- describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/set-experiment-tag' do
- let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/set-experiment-tag" }
- let(:default_params) { { experiment_id: experiment.iid.to_s, key: 'some_key', value: 'value' } }
- let(:params) { default_params }
- let(:request) { post api(route), params: params, headers: headers }
-
- it 'logs the tag', :aggregate_failures do
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_empty
- expect(experiment.reload.metadata.map(&:name)).to include('some_key')
- end
-
- describe 'Error Cases' do
- context 'when tag was already set' do
- let(:params) { default_params.merge(key: experiment.metadata[0].name) }
-
- it_behaves_like 'Bad Request'
- end
-
- it_behaves_like 'shared error cases'
- it_behaves_like 'Requires api scope'
- it_behaves_like 'Bad Request on missing required', [:key, :value]
- end
- end
-
- describe 'Runs' do
- describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/create' do
- let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/create" }
- let(:params) do
- {
- experiment_id: experiment.iid.to_s,
- start_time: Time.now.to_i,
- run_name: "A new Run",
- tags: [
- { key: 'hello', value: 'world' }
- ]
- }
- end
-
- let(:request) { post api(route), params: params, headers: headers }
-
- it 'creates the run', :aggregate_failures do
- expected_properties = {
- 'experiment_id' => params[:experiment_id],
- 'user_id' => current_user.id.to_s,
- 'run_name' => "A new Run",
- 'start_time' => params[:start_time],
- 'status' => 'RUNNING',
- 'lifecycle_stage' => 'active'
- }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('ml/run')
- expect(json_response['run']).to include('info' => hash_including(**expected_properties),
- 'data' => {
- 'metrics' => [],
- 'params' => [],
- 'tags' => [{ 'key' => 'hello', 'value' => 'world' }]
- })
- end
-
- describe 'Error States' do
- context 'when experiment id is not passed' do
- let(:params) { {} }
-
- it_behaves_like 'Bad Request'
- end
-
- context 'when experiment id does not exist' do
- let(:params) { { experiment_id: non_existing_record_iid.to_s } }
-
- it_behaves_like 'Not Found - Resource Does Not Exist'
- end
-
- context 'when experiment exists but is not part of the project' do
- let(:project_id) { another_project.id }
-
- it_behaves_like 'Not Found - Resource Does Not Exist'
- end
-
- it_behaves_like 'shared error cases'
- it_behaves_like 'Requires api scope'
- end
- end
-
- describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/runs/get' do
- let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/get" }
- let(:default_params) { { 'run_id' => candidate.iid } }
-
- it 'gets the run', :aggregate_failures do
- expected_properties = {
- 'experiment_id' => candidate.experiment.iid.to_s,
- 'user_id' => candidate.user.id.to_s,
- 'start_time' => candidate.start_time,
- 'artifact_uri' => "http://www.example.com/api/v4/projects/#{project_id}/packages/generic/ml_candidate_#{candidate.id}/-/",
- 'status' => "RUNNING",
- 'lifecycle_stage' => "active"
- }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('ml/run')
- expect(json_response['run']).to include(
- 'info' => hash_including(**expected_properties),
- 'data' => {
- 'metrics' => [
- hash_including('key' => candidate.metrics[0].name),
- hash_including('key' => candidate.metrics[1].name)
- ],
- 'params' => [
- { 'key' => candidate.params[0].name, 'value' => candidate.params[0].value },
- { 'key' => candidate.params[1].name, 'value' => candidate.params[1].value }
- ],
- 'tags' => [
- { 'key' => candidate.metadata[0].name, 'value' => candidate.metadata[0].value },
- { 'key' => candidate.metadata[1].name, 'value' => candidate.metadata[1].value }
- ]
- })
- end
-
- describe 'Error States' do
- it_behaves_like 'run_id param error cases'
- it_behaves_like 'shared error cases'
- it_behaves_like 'Requires read_api scope'
- end
- end
-
- describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/update' do
- let(:default_params) { { run_id: candidate.iid.to_s, status: 'FAILED', end_time: Time.now.to_i } }
- let(:request) { post api(route), params: params, headers: headers }
- let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/update" }
-
- it 'updates the run', :aggregate_failures do
- expected_properties = {
- 'experiment_id' => candidate.experiment.iid.to_s,
- 'user_id' => candidate.user.id.to_s,
- 'start_time' => candidate.start_time,
- 'end_time' => params[:end_time],
- 'artifact_uri' => "http://www.example.com/api/v4/projects/#{project_id}/packages/generic/ml_candidate_#{candidate.id}/-/",
- 'status' => 'FAILED',
- 'lifecycle_stage' => 'active'
- }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('ml/update_run')
- expect(json_response).to include('run_info' => hash_including(**expected_properties))
- end
-
- describe 'Error States' do
- context 'when status in invalid' do
- let(:params) { default_params.merge(status: 'YOLO') }
-
- it_behaves_like 'Bad Request'
- end
-
- context 'when end_time is invalid' do
- let(:params) { default_params.merge(end_time: 's') }
-
- it_behaves_like 'Bad Request'
- end
-
- it_behaves_like 'shared error cases'
- it_behaves_like 'Requires api scope'
- it_behaves_like 'run_id param error cases'
- end
- end
-
- describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/log-metric' do
- let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/log-metric" }
- let(:default_params) { { run_id: candidate.iid.to_s, key: 'some_key', value: 10.0, timestamp: Time.now.to_i } }
- let(:request) { post api(route), params: params, headers: headers }
-
- it 'logs the metric', :aggregate_failures do
- candidate.metrics.reload
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_empty
- expect(candidate.metrics.length).to eq(3)
- end
-
- describe 'Error Cases' do
- it_behaves_like 'shared error cases'
- it_behaves_like 'Requires api scope'
- it_behaves_like 'run_id param error cases'
- it_behaves_like 'Bad Request on missing required', [:key, :value, :timestamp]
- end
- end
-
- describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/log-parameter' do
- let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/log-parameter" }
- let(:default_params) { { run_id: candidate.iid.to_s, key: 'some_key', value: 'value' } }
- let(:request) { post api(route), params: params, headers: headers }
-
- it 'logs the parameter', :aggregate_failures do
- candidate.params.reload
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_empty
- expect(candidate.params.length).to eq(3)
- end
-
- describe 'Error Cases' do
- context 'when parameter was already logged' do
- let(:params) { default_params.tap { |p| p[:key] = candidate.params[0].name } }
-
- it_behaves_like 'Bad Request'
- end
-
- it_behaves_like 'shared error cases'
- it_behaves_like 'Requires api scope'
- it_behaves_like 'run_id param error cases'
- it_behaves_like 'Bad Request on missing required', [:key, :value]
- end
- end
-
- describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/set-tag' do
- let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/set-tag" }
- let(:default_params) { { run_id: candidate.iid.to_s, key: 'some_key', value: 'value' } }
- let(:request) { post api(route), params: params, headers: headers }
-
- it 'logs the tag', :aggregate_failures do
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_empty
- expect(candidate.reload.metadata.map(&:name)).to include('some_key')
- end
-
- describe 'Error Cases' do
- context 'when tag was already logged' do
- let(:params) { default_params.tap { |p| p[:key] = candidate.metadata[0].name } }
-
- it_behaves_like 'Bad Request'
- end
-
- it_behaves_like 'shared error cases'
- it_behaves_like 'Requires api scope'
- it_behaves_like 'run_id param error cases'
- it_behaves_like 'Bad Request on missing required', [:key, :value]
- end
- end
-
- describe 'POST /projects/:id/ml/mlflow/api/2.0/mlflow/runs/log-batch' do
- let(:candidate2) do
- create(:ml_candidates, user: experiment.user, start_time: 1234, experiment: experiment)
- end
-
- let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/runs/log-batch" }
- let(:default_params) do
- {
- run_id: candidate2.iid.to_s,
- metrics: [
- { key: 'mae', value: 2.5, timestamp: 1552550804 },
- { key: 'rmse', value: 2.7, timestamp: 1552550804 }
- ],
- params: [{ key: 'model_class', value: 'LogisticRegression' }],
- tags: [{ key: 'tag1', value: 'tag.value.1' }]
- }
- end
-
- let(:request) { post api(route), params: params, headers: headers }
-
- it 'logs parameters and metrics', :aggregate_failures do
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_empty
- expect(candidate2.params.size).to eq(1)
- expect(candidate2.metadata.size).to eq(1)
- expect(candidate2.metrics.size).to eq(2)
- end
-
- context 'when parameter was already logged' do
- let(:params) do
- default_params.tap { |p| p[:params] = [{ key: 'hello', value: 'a' }, { key: 'hello', value: 'b' }] }
- end
-
- it 'does not log', :aggregate_failures do
- candidate.params.reload
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(candidate2.params.size).to eq(1)
- end
- end
-
- context 'when tag was already logged' do
- let(:params) do
- default_params.tap { |p| p[:tags] = [{ key: 'tag1', value: 'a' }, { key: 'tag1', value: 'b' }] }
- end
-
- it 'logs only 1', :aggregate_failures do
- candidate.metadata.reload
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(candidate2.metadata.size).to eq(1)
- end
- end
-
- describe 'Error Cases' do
- context 'when required metric key is missing' do
- let(:params) { default_params.tap { |p| p[:metrics] = [p[:metrics][0].delete(:key)] } }
-
- it_behaves_like 'Bad Request'
- end
-
- context 'when required param key is missing' do
- let(:params) { default_params.tap { |p| p[:params] = [p[:params][0].delete(:key)] } }
-
- it_behaves_like 'Bad Request'
- end
-
- it_behaves_like 'shared error cases'
- it_behaves_like 'Requires api scope'
- it_behaves_like 'run_id param error cases'
- end
- end
- end
-end
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 44574caf54a..f268a092034 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -2,25 +2,27 @@
require 'spec_helper'
-RSpec.describe API::Namespaces, feature_category: :subgroups do
+RSpec.describe API::Namespaces, :aggregate_failures, feature_category: :subgroups do
let_it_be(:admin) { create(:admin) }
let_it_be(:user) { create(:user) }
let_it_be(:group1) { create(:group, name: 'group.one') }
let_it_be(:group2) { create(:group, :nested) }
let_it_be(:project) { create(:project, namespace: group2, name: group2.name, path: group2.path) }
let_it_be(:project_namespace) { project.project_namespace }
+ let_it_be(:path) { "/namespaces" }
describe "GET /namespaces" do
context "when unauthenticated" do
it "returns authentication error" do
- get api("/namespaces")
+ get api(path)
+
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context "when authenticated as admin" do
it "returns correct attributes" do
- get api("/namespaces", admin)
+ get api(path, admin, admin_mode: true)
group_kind_json_response = json_response.find { |resource| resource['kind'] == 'group' }
user_kind_json_response = json_response.find { |resource| resource['kind'] == 'user' }
@@ -34,7 +36,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
end
it "admin: returns an array of all namespaces" do
- get api("/namespaces", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -44,7 +46,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
end
it "admin: returns an array of matched namespaces" do
- get api("/namespaces?search=#{group2.name}", admin)
+ get api("/namespaces?search=#{group2.name}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -59,7 +61,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
it "returns correct attributes when user can admin group" do
group1.add_owner(user)
- get api("/namespaces", user)
+ get api(path, user)
owned_group_response = json_response.find { |resource| resource['id'] == group1.id }
@@ -70,7 +72,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
it "returns correct attributes when user cannot admin group" do
group1.add_guest(user)
- get api("/namespaces", user)
+ get api(path, user)
guest_group_response = json_response.find { |resource| resource['id'] == group1.id }
@@ -78,7 +80,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
end
it "user: returns an array of namespaces" do
- get api("/namespaces", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -115,9 +117,19 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
let_it_be(:user2) { create(:user) }
- shared_examples 'can access namespace' do
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:path) { "/namespaces/#{group2.id}" }
+ let(:failed_status_code) { :not_found }
+ end
+
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:path) { "/namespaces/#{user2.namespace.id}" }
+ let(:failed_status_code) { :not_found }
+ end
+
+ shared_examples 'can access namespace' do |admin_mode: false|
it 'returns namespace details' do
- get api("/namespaces/#{namespace_id}", request_actor)
+ get api("#{path}/#{namespace_id}", request_actor, admin_mode: admin_mode)
expect(response).to have_gitlab_http_status(:ok)
@@ -153,7 +165,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
let(:namespace_id) { project_namespace.id }
it 'returns not-found' do
- get api("/namespaces/#{namespace_id}", request_actor)
+ get api("#{path}/#{namespace_id}", request_actor)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -188,7 +200,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
context "when namespace doesn't exist" do
it 'returns not-found' do
- get api('/namespaces/0', request_actor)
+ get api("#{path}/0", request_actor)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -197,13 +209,13 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
context 'when unauthenticated' do
it 'returns authentication error' do
- get api("/namespaces/#{group1.id}")
+ get api("#{path}/#{group1.id}")
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'returns authentication error' do
- get api("/namespaces/#{project_namespace.id}")
+ get api("#{path}/#{project_namespace.id}")
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -215,7 +227,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
context 'when requested namespace is not owned by user' do
context 'when requesting group' do
it 'returns not-found' do
- get api("/namespaces/#{group2.id}", request_actor)
+ get api("#{path}/#{group2.id}", request_actor)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -223,7 +235,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
context 'when requesting personal namespace' do
it 'returns not-found' do
- get api("/namespaces/#{user2.namespace.id}", request_actor)
+ get api("#{path}/#{user2.namespace.id}", request_actor)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -243,14 +255,14 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
let(:namespace_id) { group2.id }
let(:requested_namespace) { group2 }
- it_behaves_like 'can access namespace'
+ it_behaves_like 'can access namespace', admin_mode: true
end
context 'when requesting personal namespace' do
let(:namespace_id) { user2.namespace.id }
let(:requested_namespace) { user2.namespace }
- it_behaves_like 'can access namespace'
+ it_behaves_like 'can access namespace', admin_mode: true
end
end
@@ -269,7 +281,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
context 'when unauthenticated' do
it 'returns authentication error' do
- get api("/namespaces/#{namespace1.path}/exists")
+ get api("#{path}/#{namespace1.path}/exists")
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -278,7 +290,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
let(:namespace_id) { project_namespace.id }
it 'returns authentication error' do
- get api("/namespaces/#{project_namespace.path}/exists"), params: { parent_id: group2.id }
+ get api("#{path}/#{project_namespace.path}/exists"), params: { parent_id: group2.id }
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -290,12 +302,12 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
let(:current_user) { user }
def request
- get api("/namespaces/#{namespace1.path}/exists", current_user)
+ get api("#{path}/#{namespace1.path}/exists", current_user)
end
end
it 'returns JSON indicating the namespace exists and a suggestion' do
- get api("/namespaces/#{namespace1.path}/exists", user)
+ get api("#{path}/#{namespace1.path}/exists", user)
expected_json = { exists: true, suggests: ["#{namespace1.path}1"] }.to_json
expect(response).to have_gitlab_http_status(:ok)
@@ -303,7 +315,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
end
it 'supports dot in namespace path' do
- get api("/namespaces/#{namespace_with_dot.path}/exists", user)
+ get api("#{path}/#{namespace_with_dot.path}/exists", user)
expected_json = { exists: true, suggests: ["#{namespace_with_dot.path}1"] }.to_json
expect(response).to have_gitlab_http_status(:ok)
@@ -311,7 +323,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
end
it 'returns JSON indicating the namespace does not exist without a suggestion' do
- get api("/namespaces/non-existing-namespace/exists", user)
+ get api("#{path}/non-existing-namespace/exists", user)
expected_json = { exists: false, suggests: [] }.to_json
expect(response).to have_gitlab_http_status(:ok)
@@ -319,7 +331,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
end
it 'checks the existence of a namespace in case-insensitive manner' do
- get api("/namespaces/#{namespace1.path.upcase}/exists", user)
+ get api("#{path}/#{namespace1.path.upcase}/exists", user)
expected_json = { exists: true, suggests: ["#{namespace1.path.upcase}1"] }.to_json
expect(response).to have_gitlab_http_status(:ok)
@@ -327,7 +339,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
end
it 'checks the existence within the parent namespace only' do
- get api("/namespaces/#{namespace1sub.path}/exists", user), params: { parent_id: namespace1.id }
+ get api("#{path}/#{namespace1sub.path}/exists", user), params: { parent_id: namespace1.id }
expected_json = { exists: true, suggests: ["#{namespace1sub.path}1"] }.to_json
expect(response).to have_gitlab_http_status(:ok)
@@ -335,7 +347,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
end
it 'ignores nested namespaces when checking for top-level namespace' do
- get api("/namespaces/#{namespace1sub.path}/exists", user)
+ get api("#{path}/#{namespace1sub.path}/exists", user)
expected_json = { exists: false, suggests: [] }.to_json
expect(response).to have_gitlab_http_status(:ok)
@@ -349,7 +361,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
create(:group, name: 'mygroup', path: 'mygroup', parent: namespace1)
- get api("/namespaces/mygroup/exists", user), params: { parent_id: namespace1.id }
+ get api("#{path}/mygroup/exists", user), params: { parent_id: namespace1.id }
# if the paths of groups present in hierachies aren't ignored, the suggestion generated would have
# been `mygroup3`, just because groups with path `mygroup1` and `mygroup2` exists somewhere else.
@@ -361,7 +373,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
end
it 'ignores top-level namespaces when checking with parent_id' do
- get api("/namespaces/#{namespace1.path}/exists", user), params: { parent_id: namespace1.id }
+ get api("#{path}/#{namespace1.path}/exists", user), params: { parent_id: namespace1.id }
expected_json = { exists: false, suggests: [] }.to_json
expect(response).to have_gitlab_http_status(:ok)
@@ -369,7 +381,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
end
it 'ignores namespaces of other parent namespaces when checking with parent_id' do
- get api("/namespaces/#{namespace2sub.path}/exists", user), params: { parent_id: namespace1.id }
+ get api("#{path}/#{namespace2sub.path}/exists", user), params: { parent_id: namespace1.id }
expected_json = { exists: false, suggests: [] }.to_json
expect(response).to have_gitlab_http_status(:ok)
@@ -380,7 +392,7 @@ RSpec.describe API::Namespaces, feature_category: :subgroups do
let(:namespace_id) { project_namespace.id }
it 'returns JSON indicating the namespace does not exist without a suggestion' do
- get api("/namespaces/#{project_namespace.path}/exists", user), params: { parent_id: group2.id }
+ get api("#{path}/#{project_namespace.path}/exists", user), params: { parent_id: group2.id }
expected_json = { exists: false, suggests: [] }.to_json
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index c0276e02eb7..d535629ea0d 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -70,7 +70,7 @@ RSpec.describe API::Notes, feature_category: :team_planning do
describe "GET /projects/:id/noteable/:noteable_id/notes" do
context "current user cannot view the notes" do
- it "returns an empty array" do
+ it "returns an empty array", :aggregate_failures do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -93,7 +93,7 @@ RSpec.describe API::Notes, feature_category: :team_planning do
end
context "current user can view the note" do
- it "returns a non-empty array" do
+ it "returns a non-empty array", :aggregate_failures do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", private_user)
expect(response).to have_gitlab_http_status(:ok)
@@ -114,7 +114,7 @@ RSpec.describe API::Notes, feature_category: :team_planning do
let(:test_url) { "/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes" }
shared_examples 'a notes request' do
- it 'is a note array response' do
+ it 'is a note array response', :aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
@@ -164,7 +164,7 @@ RSpec.describe API::Notes, feature_category: :team_planning do
it_behaves_like 'a notes request'
- it "properly filters the returned notables" do
+ it "properly filters the returned notables", :aggregate_failures do
expect(json_response.count).to eq(count)
expect(json_response.first["system"]).to be system_notable
end
@@ -195,7 +195,7 @@ RSpec.describe API::Notes, feature_category: :team_planning do
end
context "current user can view the note" do
- it "returns an issue note by id" do
+ it "returns an issue note by id", :aggregate_failures do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes/#{cross_reference_note.id}", private_user)
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/requests/api/npm_instance_packages_spec.rb b/spec/requests/api/npm_instance_packages_spec.rb
index dcd2e4ae677..591a8ee68dc 100644
--- a/spec/requests/api/npm_instance_packages_spec.rb
+++ b/spec/requests/api/npm_instance_packages_spec.rb
@@ -11,8 +11,38 @@ RSpec.describe API::NpmInstancePackages, feature_category: :package_registry do
include_context 'npm api setup'
describe 'GET /api/v4/packages/npm/*package_name' do
- it_behaves_like 'handling get metadata requests', scope: :instance do
- let(:url) { api("/packages/npm/#{package_name}") }
+ let(:url) { api("/packages/npm/#{package_name}") }
+
+ it_behaves_like 'handling get metadata requests', scope: :instance
+
+ context 'with a duplicate package name in another project' do
+ subject { get(url) }
+
+ let_it_be(:project2) { create(:project, :public, namespace: namespace) }
+ let_it_be(:package2) do
+ create(:npm_package,
+ project: project2,
+ name: "@#{group.path}/scoped_package",
+ version: '1.2.0')
+ end
+
+ it 'includes all matching package versions in the response' do
+ subject
+
+ expect(json_response['versions'].keys).to match_array([package.version, package2.version])
+ end
+
+ context 'with the feature flag disabled' do
+ before do
+ stub_feature_flags(npm_allow_packages_in_multiple_projects: false)
+ end
+
+ it 'returns matching package versions from only one project' do
+ subject
+
+ expect(json_response['versions'].keys).to match_array([package2.version])
+ end
+ end
end
end
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index c62c0849776..1f5ebc80824 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
+ include ExclusiveLeaseHelpers
+
include_context 'npm api setup'
shared_examples 'accept get request on private project with access to package registry for everyone' do
@@ -115,6 +117,8 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
end
context 'private project' do
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_npm_user' } }
+
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
@@ -143,6 +147,8 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
end
context 'internal project' do
+ let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_npm_user' } }
+
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
@@ -208,6 +214,14 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
it_behaves_like 'not a package tracking event'
end
end
+
+ context 'invalid package attachment data' do
+ let(:package_name) { "@#{group.path}/my_package_name" }
+ let(:params) { upload_params(package_name: package_name, file: 'npm/payload_with_empty_attachment.json') }
+
+ it_behaves_like 'handling invalid record with 400 error'
+ it_behaves_like 'not a package tracking event'
+ end
end
context 'valid package params' do
@@ -220,15 +234,7 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
context 'with access token' do
it_behaves_like 'a package tracking event', 'API::NpmPackages', 'push_package'
- it 'creates npm package with file' do
- expect { subject }
- .to change { project.packages.count }.by(1)
- .and change { Packages::PackageFile.count }.by(1)
- .and change { Packages::Tag.count }.by(1)
- .and change { Packages::Npm::Metadatum.count }.by(1)
-
- expect(response).to have_gitlab_http_status(:ok)
- end
+ it_behaves_like 'a successful package creation'
end
it 'creates npm package with file with job token' do
@@ -364,12 +370,13 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
end
end
- context 'with a too large metadata structure' do
- let(:package_name) { "@#{group.path}/my_package_name" }
- let(:params) do
- upload_params(package_name: package_name, package_version: '1.2.3').tap do |h|
- h['versions']['1.2.3']['test'] = 'test' * 10000
- end
+ context 'when the lease to create a package is already taken' do
+ let(:version) { '1.0.1' }
+ let(:params) { upload_params(package_name: package_name, package_version: version) }
+ let(:lease_key) { "packages:npm:create_package_service:packages:#{project.id}_#{package_name}_#{version}" }
+
+ before do
+ stub_exclusive_lease_taken(lease_key, timeout: Packages::Npm::CreatePackageService::DEFAULT_LEASE_TIMEOUT)
end
it_behaves_like 'not a package tracking event'
@@ -379,7 +386,95 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
.not_to change { project.packages.count }
expect(response).to have_gitlab_http_status(:bad_request)
- expect(response.body).to include('Validation failed: Package json structure is too large')
+ expect(response.body).to include('Could not obtain package lease.')
+ end
+ end
+
+ context 'with a too large metadata structure' do
+ let(:package_name) { "@#{group.path}/my_package_name" }
+
+ ::Packages::Npm::CreatePackageService::PACKAGE_JSON_NOT_ALLOWED_FIELDS.each do |field|
+ context "when a large value for #{field} is set" do
+ let(:params) do
+ upload_params(package_name: package_name, package_version: '1.2.3').tap do |h|
+ h['versions']['1.2.3'][field] = 'test' * 10000
+ end
+ end
+
+ it_behaves_like 'a successful package creation'
+ end
+ end
+
+ context 'when the large field is not one of the ignored fields' do
+ let(:params) do
+ upload_params(package_name: package_name, package_version: '1.2.3').tap do |h|
+ h['versions']['1.2.3']['test'] = 'test' * 10000
+ end
+ end
+
+ it_behaves_like 'not a package tracking event'
+
+ it 'returns an error' do
+ expect { upload_package_with_token }
+ .not_to change { project.packages.count }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response.body).to include('Validation failed: Package json structure is too large')
+ end
+ end
+ end
+
+ context 'when the Npm-Command in headers is deprecate' do
+ let(:package_name) { "@#{group.path}/my_package_name" }
+ let(:headers) { build_token_auth_header(token.plaintext_token).merge('Npm-Command' => 'deprecate') }
+ let(:params) do
+ {
+ 'id' => project.id.to_s,
+ 'package_name' => package_name,
+ 'versions' => {
+ '1.0.1' => {
+ 'name' => package_name,
+ 'deprecated' => 'This version is deprecated'
+ },
+ '1.0.2' => {
+ 'name' => package_name
+ }
+ }
+ }
+ end
+
+ subject(:request) { put api("/projects/#{project.id}/packages/npm/#{package_name.sub('/', '%2f')}"), params: params, headers: headers }
+
+ context 'when the user is not authorized to destroy the package' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'does not call DeprecatePackageService' do
+ expect(::Packages::Npm::DeprecatePackageService).not_to receive(:new)
+
+ request
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when the user is authorized to destroy the package' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'calls DeprecatePackageService with the correct arguments' do
+ expect(::Packages::Npm::DeprecatePackageService).to receive(:new).with(project, params) do
+ double.tap do |service|
+ expect(service).to receive(:execute).with(async: true)
+ end
+ end
+
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
end
end
diff --git a/spec/requests/api/nuget_group_packages_spec.rb b/spec/requests/api/nuget_group_packages_spec.rb
index 4335ad75ab6..facbc01220d 100644
--- a/spec/requests/api/nuget_group_packages_spec.rb
+++ b/spec/requests/api/nuget_group_packages_spec.rb
@@ -12,8 +12,17 @@ RSpec.describe API::NugetGroupPackages, feature_category: :package_registry do
let_it_be(:deploy_token) { create(:deploy_token, :group, read_package_registry: true, write_package_registry: true) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token, group: group) }
- let(:snowplow_gitlab_standard_context) { { namespace: project.group, property: 'i_package_nuget_user' } }
let(:target_type) { 'groups' }
+ let(:snowplow_gitlab_standard_context) { snowplow_context }
+ let(:target) { subgroup }
+
+ def snowplow_context(user_role: :developer)
+ if user_role == :anonymous
+ { namespace: target, property: 'i_package_nuget_user' }
+ else
+ { namespace: target, property: 'i_package_nuget_user', user: user }
+ end
+ end
shared_examples 'handling all endpoints' do
describe 'GET /api/v4/groups/:id/-/packages/nuget' do
@@ -84,7 +93,6 @@ RSpec.describe API::NugetGroupPackages, feature_category: :package_registry do
context 'a group' do
let(:target) { group }
- let(:snowplow_gitlab_standard_context) { { namespace: target, property: 'i_package_nuget_user' } }
it_behaves_like 'handling all endpoints'
diff --git a/spec/requests/api/nuget_project_packages_spec.rb b/spec/requests/api/nuget_project_packages_spec.rb
index 1e0d35ad451..887dfd4beeb 100644
--- a/spec/requests/api/nuget_project_packages_spec.rb
+++ b/spec/requests/api/nuget_project_packages_spec.rb
@@ -13,7 +13,15 @@ RSpec.describe API::NugetProjectPackages, feature_category: :package_registry do
let(:target) { project }
let(:target_type) { 'projects' }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_nuget_user' } }
+ let(:snowplow_gitlab_standard_context) { snowplow_context }
+
+ def snowplow_context(user_role: :developer)
+ if user_role == :anonymous
+ { project: target, namespace: target.namespace, property: 'i_package_nuget_user' }
+ else
+ { project: target, namespace: target.namespace, property: 'i_package_nuget_user', user: user }
+ end
+ end
shared_examples 'accept get request on private project with access to package registry for everyone' do
subject { get api(url) }
@@ -149,6 +157,7 @@ RSpec.describe API::NugetProjectPackages, feature_category: :package_registry do
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
subject { get api(url), headers: headers }
diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb
index b29f1e9e661..19a943477d2 100644
--- a/spec/requests/api/oauth_tokens_spec.rb
+++ b/spec/requests/api/oauth_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'OAuth tokens', feature_category: :authentication_and_authorization do
+RSpec.describe 'OAuth tokens', feature_category: :system_access do
include HttpBasicAuthHelpers
context 'Resource Owner Password Credentials' do
@@ -124,6 +124,8 @@ RSpec.describe 'OAuth tokens', feature_category: :authentication_and_authorizati
context 'when user account is not confirmed' do
before do
+ stub_application_setting_enum('email_confirmation_setting', 'soft')
+
user.update!(confirmed_at: nil)
request_oauth_token(user, client_basic_auth_header(client))
diff --git a/spec/requests/api/package_files_spec.rb b/spec/requests/api/package_files_spec.rb
index f47dca387ef..3b4bd2f3cf4 100644
--- a/spec/requests/api/package_files_spec.rb
+++ b/spec/requests/api/package_files_spec.rb
@@ -10,6 +10,15 @@ RSpec.describe API::PackageFiles, feature_category: :package_registry do
describe 'GET /projects/:id/packages/:package_id/package_files' do
let(:url) { "/projects/#{project.id}/packages/#{package.id}/package_files" }
+ shared_examples 'handling job token and returning' do |status:|
+ it "returns status #{status}" do
+ get api(url, job_token: job.token)
+
+ expect(response).to have_gitlab_http_status(status)
+ expect(response).to match_response_schema('public_api/v4/packages/package_files') if status == :ok
+ end
+ end
+
before do
project.add_developer(user)
end
@@ -27,6 +36,12 @@ RSpec.describe API::PackageFiles, feature_category: :package_registry do
expect(response).to have_gitlab_http_status(:not_found)
end
+
+ context 'with JOB-TOKEN auth' do
+ let(:job) { create(:ci_build, :running, user: user, project: project) }
+
+ it_behaves_like 'handling job token and returning', status: :ok
+ end
end
context 'project is private' do
@@ -52,6 +67,28 @@ RSpec.describe API::PackageFiles, feature_category: :package_registry do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/packages/package_files')
end
+
+ context 'with JOB-TOKEN auth' do
+ let(:job) { create(:ci_build, :running, user: user, project: project) }
+
+ context 'a non authenticated user' do
+ let(:user) { nil }
+
+ it_behaves_like 'handling job token and returning', status: :not_found
+ end
+
+ context 'a user without access to the project', :sidekiq_inline do
+ before do
+ project.team.truncate
+ end
+
+ it_behaves_like 'handling job token and returning', status: :not_found
+ end
+
+ context 'a user with access to the project' do
+ it_behaves_like 'handling job token and returning', status: :ok
+ end
+ end
end
context 'with pagination params' do
@@ -97,6 +134,18 @@ RSpec.describe API::PackageFiles, feature_category: :package_registry do
subject(:api_request) { delete api(url, user) }
+ shared_examples 'handling job token and returning' do |status:|
+ it "returns status #{status}", :aggregate_failures do
+ if status == :no_content
+ expect { api_request }.to change { package.package_files.pending_destruction.count }.by(1)
+ else
+ expect { api_request }.not_to change { package.package_files.pending_destruction.count }
+ end
+
+ expect(response).to have_gitlab_http_status(status)
+ end
+ end
+
context 'project is public' do
context 'without user' do
let(:user) { nil }
@@ -108,6 +157,14 @@ RSpec.describe API::PackageFiles, feature_category: :package_registry do
end
end
+ context 'with JOB-TOKEN auth' do
+ subject(:api_request) { delete api(url, job_token: job.token) }
+
+ let(:job) { create(:ci_build, :running, user: user, project: project) }
+
+ it_behaves_like 'handling job token and returning', status: :forbidden
+ end
+
it 'returns 403 for a user without access to the project', :aggregate_failures do
expect { api_request }.not_to change { package.package_files.pending_destruction.count }
@@ -175,6 +232,33 @@ RSpec.describe API::PackageFiles, feature_category: :package_registry do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'with JOB-TOKEN auth' do
+ subject(:api_request) { delete api(url, job_token: job.token) }
+
+ let(:job) { create(:ci_build, :running, user: user, project: project) }
+ let_it_be_with_refind(:project) { create(:project, :private) }
+
+ context 'a user without access to the project' do
+ it_behaves_like 'handling job token and returning', status: :not_found
+ end
+
+ context 'a user without enough permissions' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'handling job token and returning', status: :forbidden
+ end
+
+ context 'a user with the right permissions' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'handling job token and returning', status: :no_content
+ end
+ end
end
end
end
diff --git a/spec/requests/api/pages/internal_access_spec.rb b/spec/requests/api/pages/internal_access_spec.rb
index fdc25ecdcd3..3e7837866ae 100644
--- a/spec/requests/api/pages/internal_access_spec.rb
+++ b/spec/requests/api/pages/internal_access_spec.rb
@@ -35,39 +35,39 @@ RSpec.describe "Internal Project Pages Access", feature_category: :pages do
describe "GET /projects/:id/pages_access" do
context 'access depends on the level' do
- where(:pages_access_level, :with_user, :expected_result) do
- ProjectFeature::DISABLED | "admin" | 403
- ProjectFeature::DISABLED | "owner" | 403
- ProjectFeature::DISABLED | "master" | 403
- ProjectFeature::DISABLED | "developer" | 403
- ProjectFeature::DISABLED | "reporter" | 403
- ProjectFeature::DISABLED | "guest" | 403
- ProjectFeature::DISABLED | "user" | 403
- ProjectFeature::DISABLED | nil | 404
- ProjectFeature::PUBLIC | "admin" | 200
- ProjectFeature::PUBLIC | "owner" | 200
- ProjectFeature::PUBLIC | "master" | 200
- ProjectFeature::PUBLIC | "developer" | 200
- ProjectFeature::PUBLIC | "reporter" | 200
- ProjectFeature::PUBLIC | "guest" | 200
- ProjectFeature::PUBLIC | "user" | 200
- ProjectFeature::PUBLIC | nil | 404
- ProjectFeature::ENABLED | "admin" | 200
- ProjectFeature::ENABLED | "owner" | 200
- ProjectFeature::ENABLED | "master" | 200
- ProjectFeature::ENABLED | "developer" | 200
- ProjectFeature::ENABLED | "reporter" | 200
- ProjectFeature::ENABLED | "guest" | 200
- ProjectFeature::ENABLED | "user" | 200
- ProjectFeature::ENABLED | nil | 404
- ProjectFeature::PRIVATE | "admin" | 200
- ProjectFeature::PRIVATE | "owner" | 200
- ProjectFeature::PRIVATE | "master" | 200
- ProjectFeature::PRIVATE | "developer" | 200
- ProjectFeature::PRIVATE | "reporter" | 200
- ProjectFeature::PRIVATE | "guest" | 200
- ProjectFeature::PRIVATE | "user" | 403
- ProjectFeature::PRIVATE | nil | 404
+ where(:pages_access_level, :with_user, :admin_mode, :expected_result) do
+ ProjectFeature::DISABLED | "admin" | true | 403
+ ProjectFeature::DISABLED | "owner" | false | 403
+ ProjectFeature::DISABLED | "master" | false | 403
+ ProjectFeature::DISABLED | "developer" | false | 403
+ ProjectFeature::DISABLED | "reporter" | false | 403
+ ProjectFeature::DISABLED | "guest" | false | 403
+ ProjectFeature::DISABLED | "user" | false | 403
+ ProjectFeature::DISABLED | nil | false | 404
+ ProjectFeature::PUBLIC | "admin" | false | 200
+ ProjectFeature::PUBLIC | "owner" | false | 200
+ ProjectFeature::PUBLIC | "master" | false | 200
+ ProjectFeature::PUBLIC | "developer" | false | 200
+ ProjectFeature::PUBLIC | "reporter" | false | 200
+ ProjectFeature::PUBLIC | "guest" | false | 200
+ ProjectFeature::PUBLIC | "user" | false | 200
+ ProjectFeature::PUBLIC | nil | false | 404
+ ProjectFeature::ENABLED | "admin" | false | 200
+ ProjectFeature::ENABLED | "owner" | false | 200
+ ProjectFeature::ENABLED | "master" | false | 200
+ ProjectFeature::ENABLED | "developer" | false | 200
+ ProjectFeature::ENABLED | "reporter" | false | 200
+ ProjectFeature::ENABLED | "guest" | false | 200
+ ProjectFeature::ENABLED | "user" | false | 200
+ ProjectFeature::ENABLED | nil | false | 404
+ ProjectFeature::PRIVATE | "admin" | true | 200
+ ProjectFeature::PRIVATE | "owner" | false | 200
+ ProjectFeature::PRIVATE | "master" | false | 200
+ ProjectFeature::PRIVATE | "developer" | false | 200
+ ProjectFeature::PRIVATE | "reporter" | false | 200
+ ProjectFeature::PRIVATE | "guest" | false | 200
+ ProjectFeature::PRIVATE | "user" | false | 403
+ ProjectFeature::PRIVATE | nil | false | 404
end
with_them do
@@ -77,7 +77,7 @@ RSpec.describe "Internal Project Pages Access", feature_category: :pages do
it "correct return value" do
if !with_user.nil?
user = public_send(with_user)
- get api("/projects/#{project.id}/pages_access", user)
+ get api("/projects/#{project.id}/pages_access", user, admin_mode: admin_mode)
else
get api("/projects/#{project.id}/pages_access")
end
diff --git a/spec/requests/api/pages/pages_spec.rb b/spec/requests/api/pages/pages_spec.rb
index c426f2a433c..aa1869eaa84 100644
--- a/spec/requests/api/pages/pages_spec.rb
+++ b/spec/requests/api/pages/pages_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe API::Pages, feature_category: :pages do
- let_it_be(:project) { create(:project, path: 'my.project', pages_https_only: false) }
+ let_it_be_with_reload(:project) { create(:project, path: 'my.project', pages_https_only: false) }
let_it_be(:admin) { create(:admin) }
let_it_be(:user) { create(:user) }
@@ -13,13 +13,23 @@ RSpec.describe API::Pages, feature_category: :pages do
end
describe 'DELETE /projects/:id/pages' do
+ let(:path) { "/projects/#{project.id}/pages" }
+
+ it_behaves_like 'DELETE request permissions for admin mode' do
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ end
+
+ let(:succes_status_code) { :no_content }
+ end
+
context 'when Pages is disabled' do
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
end
it_behaves_like '404 response' do
- let(:request) { delete api("/projects/#{project.id}/pages", admin) }
+ let(:request) { delete api(path, admin, admin_mode: true) }
end
end
@@ -30,13 +40,13 @@ RSpec.describe API::Pages, feature_category: :pages do
context 'when Pages are deployed' do
it 'returns 204' do
- delete api("/projects/#{project.id}/pages", admin)
+ delete api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
end
it 'removes the pages' do
- delete api("/projects/#{project.id}/pages", admin)
+ delete api(path, admin, admin_mode: true)
expect(project.reload.pages_metadatum.deployed?).to be(false)
end
@@ -48,7 +58,7 @@ RSpec.describe API::Pages, feature_category: :pages do
end
it 'returns 204' do
- delete api("/projects/#{project.id}/pages", admin)
+ delete api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
end
@@ -58,7 +68,7 @@ RSpec.describe API::Pages, feature_category: :pages do
it 'returns 404' do
id = -1
- delete api("/projects/#{id}/pages", admin)
+ delete api("/projects/#{id}/pages", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/requests/api/pages/private_access_spec.rb b/spec/requests/api/pages/private_access_spec.rb
index 5cc1b8f9a69..602eff73b0a 100644
--- a/spec/requests/api/pages/private_access_spec.rb
+++ b/spec/requests/api/pages/private_access_spec.rb
@@ -35,39 +35,39 @@ RSpec.describe "Private Project Pages Access", feature_category: :pages do
describe "GET /projects/:id/pages_access" do
context 'access depends on the level' do
- where(:pages_access_level, :with_user, :expected_result) do
- ProjectFeature::DISABLED | "admin" | 403
- ProjectFeature::DISABLED | "owner" | 403
- ProjectFeature::DISABLED | "master" | 403
- ProjectFeature::DISABLED | "developer" | 403
- ProjectFeature::DISABLED | "reporter" | 403
- ProjectFeature::DISABLED | "guest" | 403
- ProjectFeature::DISABLED | "user" | 404
- ProjectFeature::DISABLED | nil | 404
- ProjectFeature::PUBLIC | "admin" | 200
- ProjectFeature::PUBLIC | "owner" | 200
- ProjectFeature::PUBLIC | "master" | 200
- ProjectFeature::PUBLIC | "developer" | 200
- ProjectFeature::PUBLIC | "reporter" | 200
- ProjectFeature::PUBLIC | "guest" | 200
- ProjectFeature::PUBLIC | "user" | 404
- ProjectFeature::PUBLIC | nil | 404
- ProjectFeature::ENABLED | "admin" | 200
- ProjectFeature::ENABLED | "owner" | 200
- ProjectFeature::ENABLED | "master" | 200
- ProjectFeature::ENABLED | "developer" | 200
- ProjectFeature::ENABLED | "reporter" | 200
- ProjectFeature::ENABLED | "guest" | 200
- ProjectFeature::ENABLED | "user" | 404
- ProjectFeature::ENABLED | nil | 404
- ProjectFeature::PRIVATE | "admin" | 200
- ProjectFeature::PRIVATE | "owner" | 200
- ProjectFeature::PRIVATE | "master" | 200
- ProjectFeature::PRIVATE | "developer" | 200
- ProjectFeature::PRIVATE | "reporter" | 200
- ProjectFeature::PRIVATE | "guest" | 200
- ProjectFeature::PRIVATE | "user" | 404
- ProjectFeature::PRIVATE | nil | 404
+ where(:pages_access_level, :with_user, :admin_mode, :expected_result) do
+ ProjectFeature::DISABLED | "admin" | true | 403
+ ProjectFeature::DISABLED | "owner" | false | 403
+ ProjectFeature::DISABLED | "master" | false | 403
+ ProjectFeature::DISABLED | "developer" | false | 403
+ ProjectFeature::DISABLED | "reporter" | false | 403
+ ProjectFeature::DISABLED | "guest" | false | 403
+ ProjectFeature::DISABLED | "user" | false | 404
+ ProjectFeature::DISABLED | nil | false | 404
+ ProjectFeature::PUBLIC | "admin" | true | 200
+ ProjectFeature::PUBLIC | "owner" | false | 200
+ ProjectFeature::PUBLIC | "master" | false | 200
+ ProjectFeature::PUBLIC | "developer" | false | 200
+ ProjectFeature::PUBLIC | "reporter" | false | 200
+ ProjectFeature::PUBLIC | "guest" | false | 200
+ ProjectFeature::PUBLIC | "user" | false | 404
+ ProjectFeature::PUBLIC | nil | false | 404
+ ProjectFeature::ENABLED | "admin" | true | 200
+ ProjectFeature::ENABLED | "owner" | false | 200
+ ProjectFeature::ENABLED | "master" | false | 200
+ ProjectFeature::ENABLED | "developer" | false | 200
+ ProjectFeature::ENABLED | "reporter" | false | 200
+ ProjectFeature::ENABLED | "guest" | false | 200
+ ProjectFeature::ENABLED | "user" | false | 404
+ ProjectFeature::ENABLED | nil | false | 404
+ ProjectFeature::PRIVATE | "admin" | true | 200
+ ProjectFeature::PRIVATE | "owner" | false | 200
+ ProjectFeature::PRIVATE | "master" | false | 200
+ ProjectFeature::PRIVATE | "developer" | false | 200
+ ProjectFeature::PRIVATE | "reporter" | false | 200
+ ProjectFeature::PRIVATE | "guest" | false | 200
+ ProjectFeature::PRIVATE | "user" | false | 404
+ ProjectFeature::PRIVATE | nil | false | 404
end
with_them do
@@ -77,7 +77,7 @@ RSpec.describe "Private Project Pages Access", feature_category: :pages do
it "correct return value" do
if !with_user.nil?
user = public_send(with_user)
- get api("/projects/#{project.id}/pages_access", user)
+ get api("/projects/#{project.id}/pages_access", user, admin_mode: admin_mode)
else
get api("/projects/#{project.id}/pages_access")
end
diff --git a/spec/requests/api/pages/public_access_spec.rb b/spec/requests/api/pages/public_access_spec.rb
index 1137f91f4b0..8b0ed7c59ab 100644
--- a/spec/requests/api/pages/public_access_spec.rb
+++ b/spec/requests/api/pages/public_access_spec.rb
@@ -35,39 +35,39 @@ RSpec.describe "Public Project Pages Access", feature_category: :pages do
describe "GET /projects/:id/pages_access" do
context 'access depends on the level' do
- where(:pages_access_level, :with_user, :expected_result) do
- ProjectFeature::DISABLED | "admin" | 403
- ProjectFeature::DISABLED | "owner" | 403
- ProjectFeature::DISABLED | "master" | 403
- ProjectFeature::DISABLED | "developer" | 403
- ProjectFeature::DISABLED | "reporter" | 403
- ProjectFeature::DISABLED | "guest" | 403
- ProjectFeature::DISABLED | "user" | 403
- ProjectFeature::DISABLED | nil | 403
- ProjectFeature::PUBLIC | "admin" | 200
- ProjectFeature::PUBLIC | "owner" | 200
- ProjectFeature::PUBLIC | "master" | 200
- ProjectFeature::PUBLIC | "developer" | 200
- ProjectFeature::PUBLIC | "reporter" | 200
- ProjectFeature::PUBLIC | "guest" | 200
- ProjectFeature::PUBLIC | "user" | 200
- ProjectFeature::PUBLIC | nil | 200
- ProjectFeature::ENABLED | "admin" | 200
- ProjectFeature::ENABLED | "owner" | 200
- ProjectFeature::ENABLED | "master" | 200
- ProjectFeature::ENABLED | "developer" | 200
- ProjectFeature::ENABLED | "reporter" | 200
- ProjectFeature::ENABLED | "guest" | 200
- ProjectFeature::ENABLED | "user" | 200
- ProjectFeature::ENABLED | nil | 200
- ProjectFeature::PRIVATE | "admin" | 200
- ProjectFeature::PRIVATE | "owner" | 200
- ProjectFeature::PRIVATE | "master" | 200
- ProjectFeature::PRIVATE | "developer" | 200
- ProjectFeature::PRIVATE | "reporter" | 200
- ProjectFeature::PRIVATE | "guest" | 200
- ProjectFeature::PRIVATE | "user" | 403
- ProjectFeature::PRIVATE | nil | 403
+ where(:pages_access_level, :with_user, :admin_mode, :expected_result) do
+ ProjectFeature::DISABLED | "admin" | false | 403
+ ProjectFeature::DISABLED | "owner" | false | 403
+ ProjectFeature::DISABLED | "master" | false | 403
+ ProjectFeature::DISABLED | "developer" | false | 403
+ ProjectFeature::DISABLED | "reporter" | false | 403
+ ProjectFeature::DISABLED | "guest" | false | 403
+ ProjectFeature::DISABLED | "user" | false | 403
+ ProjectFeature::DISABLED | nil | false | 403
+ ProjectFeature::PUBLIC | "admin" | false | 200
+ ProjectFeature::PUBLIC | "owner" | false | 200
+ ProjectFeature::PUBLIC | "master" | false | 200
+ ProjectFeature::PUBLIC | "developer" | false | 200
+ ProjectFeature::PUBLIC | "reporter" | false | 200
+ ProjectFeature::PUBLIC | "guest" | false | 200
+ ProjectFeature::PUBLIC | "user" | false | 200
+ ProjectFeature::PUBLIC | nil | false | 200
+ ProjectFeature::ENABLED | "admin" | false | 200
+ ProjectFeature::ENABLED | "owner" | false | 200
+ ProjectFeature::ENABLED | "master" | false | 200
+ ProjectFeature::ENABLED | "developer" | false | 200
+ ProjectFeature::ENABLED | "reporter" | false | 200
+ ProjectFeature::ENABLED | "guest" | false | 200
+ ProjectFeature::ENABLED | "user" | false | 200
+ ProjectFeature::ENABLED | nil | false | 200
+ ProjectFeature::PRIVATE | "admin" | true | 200
+ ProjectFeature::PRIVATE | "owner" | false | 200
+ ProjectFeature::PRIVATE | "master" | false | 200
+ ProjectFeature::PRIVATE | "developer" | false | 200
+ ProjectFeature::PRIVATE | "reporter" | false | 200
+ ProjectFeature::PRIVATE | "guest" | false | 200
+ ProjectFeature::PRIVATE | "user" | false | 403
+ ProjectFeature::PRIVATE | nil | false | 403
end
with_them do
@@ -77,7 +77,7 @@ RSpec.describe "Public Project Pages Access", feature_category: :pages do
it "correct return value" do
if !with_user.nil?
user = public_send(with_user)
- get api("/projects/#{project.id}/pages_access", user)
+ get api("/projects/#{project.id}/pages_access", user, admin_mode: admin_mode)
else
get api("/projects/#{project.id}/pages_access")
end
diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb
index ba1fb5105b8..9ca027c2edc 100644
--- a/spec/requests/api/pages_domains_spec.rb
+++ b/spec/requests/api/pages_domains_spec.rb
@@ -35,20 +35,24 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
end
describe 'GET /pages/domains' do
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:path) { '/pages/domains' }
+ end
+
context 'when pages is disabled' do
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
end
it_behaves_like '404 response' do
- let(:request) { get api('/pages/domains', admin) }
+ let(:request) { get api('/pages/domains', admin, admin_mode: true) }
end
end
context 'when pages is enabled' do
context 'when authenticated as an admin' do
- it 'returns paginated all pages domains' do
- get api('/pages/domains', admin)
+ it 'returns paginated all pages domains', :aggregate_failures do
+ get api('/pages/domains', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/pages_domain_basics')
@@ -74,7 +78,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
describe 'GET /projects/:project_id/pages/domains' do
shared_examples_for 'get pages domains' do
- it 'returns paginated pages domains' do
+ it 'returns paginated pages domains', :aggregate_failures do
get api(route, user)
expect(response).to have_gitlab_http_status(:ok)
@@ -145,7 +149,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
describe 'GET /projects/:project_id/pages/domains/:domain' do
shared_examples_for 'get pages domain' do
- it 'returns pages domain' do
+ it 'returns pages domain', :aggregate_failures do
get api(route_domain, user)
expect(response).to have_gitlab_http_status(:ok)
@@ -155,7 +159,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(json_response['certificate']).to be_nil
end
- it 'returns pages domain with project path' do
+ it 'returns pages domain with project path', :aggregate_failures do
get api(route_domain_path, user)
expect(response).to have_gitlab_http_status(:ok)
@@ -165,7 +169,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(json_response['certificate']).to be_nil
end
- it 'returns pages domain with a certificate' do
+ it 'returns pages domain with a certificate', :aggregate_failures do
get api(route_secure_domain, user)
expect(response).to have_gitlab_http_status(:ok)
@@ -177,7 +181,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(json_response['auto_ssl_enabled']).to be false
end
- it 'returns pages domain with an expired certificate' do
+ it 'returns pages domain with an expired certificate', :aggregate_failures do
get api(route_expired_domain, user)
expect(response).to have_gitlab_http_status(:ok)
@@ -185,7 +189,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(json_response['certificate']['expired']).to be true
end
- it 'returns pages domain with letsencrypt' do
+ it 'returns pages domain with letsencrypt', :aggregate_failures do
get api(route_letsencrypt_domain, user)
expect(response).to have_gitlab_http_status(:ok)
@@ -258,7 +262,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
let(:params_secure) { pages_domain_secure_params.slice(:domain, :certificate, :key) }
shared_examples_for 'post pages domains' do
- it 'creates a new pages domain' do
+ it 'creates a new pages domain', :aggregate_failures do
expect { post api(route, user), params: params }
.to publish_event(PagesDomains::PagesDomainCreatedEvent)
.with(
@@ -279,7 +283,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(pages_domain.auto_ssl_enabled).to be false
end
- it 'creates a new secure pages domain' do
+ it 'creates a new secure pages domain', :aggregate_failures do
post api(route, user), params: params_secure
pages_domain = PagesDomain.find_by(domain: json_response['domain'])
@@ -291,7 +295,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(pages_domain.auto_ssl_enabled).to be false
end
- it 'creates domain with letsencrypt enabled' do
+ it 'creates domain with letsencrypt enabled', :aggregate_failures do
post api(route, user), params: pages_domain_with_letsencrypt_params
pages_domain = PagesDomain.find_by(domain: json_response['domain'])
@@ -301,7 +305,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(pages_domain.auto_ssl_enabled).to be true
end
- it 'creates domain with letsencrypt enabled and provided certificate' do
+ it 'creates domain with letsencrypt enabled and provided certificate', :aggregate_failures do
post api(route, user), params: params_secure.merge(auto_ssl_enabled: true)
pages_domain = PagesDomain.find_by(domain: json_response['domain'])
@@ -376,7 +380,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
let(:params_secure_nokey) { pages_domain_secure_params.slice(:certificate) }
shared_examples_for 'put pages domain' do
- it 'updates pages domain removing certificate' do
+ it 'updates pages domain removing certificate', :aggregate_failures do
put api(route_secure_domain, user), params: { certificate: nil, key: nil }
pages_domain_secure.reload
@@ -399,7 +403,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
)
end
- it 'updates pages domain adding certificate' do
+ it 'updates pages domain adding certificate', :aggregate_failures do
put api(route_domain, user), params: params_secure
pages_domain.reload
@@ -409,7 +413,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(pages_domain.key).to eq(params_secure[:key])
end
- it 'updates pages domain adding certificate with letsencrypt' do
+ it 'updates pages domain adding certificate with letsencrypt', :aggregate_failures do
put api(route_domain, user), params: params_secure.merge(auto_ssl_enabled: true)
pages_domain.reload
@@ -420,7 +424,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(pages_domain.auto_ssl_enabled).to be true
end
- it 'updates pages domain enabling letsencrypt' do
+ it 'updates pages domain enabling letsencrypt', :aggregate_failures do
put api(route_domain, user), params: { auto_ssl_enabled: true }
pages_domain.reload
@@ -429,7 +433,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(pages_domain.auto_ssl_enabled).to be true
end
- it 'updates pages domain disabling letsencrypt while preserving the certificate' do
+ it 'updates pages domain disabling letsencrypt while preserving the certificate', :aggregate_failures do
put api(route_letsencrypt_domain, user), params: { auto_ssl_enabled: false }
pages_domain_with_letsencrypt.reload
@@ -440,7 +444,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(pages_domain_with_letsencrypt.certificate).to be
end
- it 'updates pages domain with expired certificate' do
+ it 'updates pages domain with expired certificate', :aggregate_failures do
put api(route_expired_domain, user), params: params_secure
pages_domain_expired.reload
@@ -450,7 +454,7 @@ RSpec.describe API::PagesDomains, feature_category: :pages do
expect(pages_domain_expired.key).to eq(params_secure[:key])
end
- it 'updates pages domain with expired certificate not updating key' do
+ it 'updates pages domain with expired certificate not updating key', :aggregate_failures do
put api(route_secure_domain, user), params: params_secure_nokey
pages_domain_secure.reload
diff --git a/spec/requests/api/personal_access_tokens/self_information_spec.rb b/spec/requests/api/personal_access_tokens/self_information_spec.rb
index 4a3c0ad8904..3cfaaaf7d3f 100644
--- a/spec/requests/api/personal_access_tokens/self_information_spec.rb
+++ b/spec/requests/api/personal_access_tokens/self_information_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::PersonalAccessTokens::SelfInformation, feature_category: :authentication_and_authorization do
+RSpec.describe API::PersonalAccessTokens::SelfInformation, feature_category: :system_access do
let(:path) { '/personal_access_tokens/self' }
let(:token) { create(:personal_access_token, user: current_user) }
@@ -12,7 +12,7 @@ RSpec.describe API::PersonalAccessTokens::SelfInformation, feature_category: :au
subject(:delete_token) { delete api(path, personal_access_token: token) }
shared_examples 'revoking token succeeds' do
- it 'revokes token' do
+ it 'revokes token', :aggregate_failures do
delete_token
expect(response).to have_gitlab_http_status(:no_content)
@@ -72,7 +72,7 @@ RSpec.describe API::PersonalAccessTokens::SelfInformation, feature_category: :au
context "with a '#{scope}' scoped token" do
let(:token) { create(:personal_access_token, scopes: [scope], user: current_user) }
- it 'shows token info' do
+ it 'shows token info', :aggregate_failures do
get api(path, personal_access_token: token)
expect(response).to have_gitlab_http_status(:ok)
diff --git a/spec/requests/api/personal_access_tokens_spec.rb b/spec/requests/api/personal_access_tokens_spec.rb
index 32adc7ebd61..166768ea605 100644
--- a/spec/requests/api/personal_access_tokens_spec.rb
+++ b/spec/requests/api/personal_access_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::PersonalAccessTokens, feature_category: :authentication_and_authorization do
+RSpec.describe API::PersonalAccessTokens, :aggregate_failures, feature_category: :system_access do
let_it_be(:path) { '/personal_access_tokens' }
describe 'GET /personal_access_tokens' do
@@ -30,9 +30,13 @@ RSpec.describe API::PersonalAccessTokens, feature_category: :authentication_and_
end
end
+ # Since all user types pass the same test successfully, we can avoid using
+ # shared examples and test each user type separately for its expected
+ # returned value.
+
context 'logged in as an Administrator' do
let_it_be(:current_user) { create(:admin) }
- let_it_be(:current_users_token) { create(:personal_access_token, user: current_user) }
+ let_it_be(:current_users_token) { create(:personal_access_token, :admin_mode, user: current_user) }
it 'returns all PATs by default' do
get api(path, current_user)
@@ -46,7 +50,7 @@ RSpec.describe API::PersonalAccessTokens, feature_category: :authentication_and_
let_it_be(:token_impersonated) { create(:personal_access_token, impersonation: true, user: token.user) }
it 'returns only PATs belonging to that user' do
- get api(path, current_user), params: { user_id: token.user.id }
+ get api(path, current_user, admin_mode: true), params: { user_id: token.user.id }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.count).to eq(2)
@@ -444,6 +448,68 @@ RSpec.describe API::PersonalAccessTokens, feature_category: :authentication_and_
end
end
+ describe 'POST /personal_access_tokens/:id/rotate' do
+ let_it_be(:token) { create(:personal_access_token) }
+
+ let(:path) { "/personal_access_tokens/#{token.id}/rotate" }
+
+ it "rotates user's own token", :freeze_time do
+ post api(path, token.user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['token']).not_to eq(token.token)
+ expect(json_response['expires_at']).to eq((Date.today + 1.week).to_s)
+ end
+
+ context 'without permission' do
+ it 'returns an error message' do
+ another_user = create(:user)
+ post api(path, another_user)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when service raises an error' do
+ let(:error_message) { 'boom!' }
+
+ before do
+ allow_next_instance_of(PersonalAccessTokens::RotateService) do |service|
+ allow(service).to receive(:execute).and_return(ServiceResponse.error(message: error_message))
+ end
+ end
+
+ it 'returns the same error message' do
+ post api(path, token.user)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq("400 Bad request - #{error_message}")
+ end
+ end
+
+ context 'when token does not exist' do
+ let(:invalid_path) { "/personal_access_tokens/#{non_existing_record_id}/rotate" }
+
+ context 'for non-admin user' do
+ it 'returns unauthorized' do
+ user = create(:user)
+ post api(invalid_path, user)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'for admin user', :enable_admin_mode do
+ it 'returns not found' do
+ admin = create(:admin)
+ post api(invalid_path, admin)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+
describe 'DELETE /personal_access_tokens/:id' do
let_it_be(:current_user) { create(:user) }
let_it_be(:token1) { create(:personal_access_token) }
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index 60406f380a5..e9581265bb0 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -42,7 +42,6 @@ itself: # project
- runners_token_encrypted
- storage_version
- topic_list
- - updated_at
- mirror_branch_regex
remapped_attributes:
avatar: avatar_url
@@ -75,6 +74,7 @@ itself: # project
- tag_list
- topics
- web_url
+ - description_html
build_auto_devops: # auto_devops
unexposed_attributes:
@@ -99,7 +99,6 @@ ci_cd_settings:
forward_deployment_enabled: ci_forward_deployment_enabled
job_token_scope_enabled: ci_job_token_scope_enabled
separated_caches: ci_separated_caches
- opt_in_jwt: ci_opt_in_jwt
allow_fork_pipelines_to_run_in_parent_project: ci_allow_fork_pipelines_to_run_in_parent_project
build_import_state: # import_state
@@ -127,6 +126,7 @@ project_feature:
- package_registry_access_level
- project_id
- updated_at
+ - operations_access_level
computed_attributes:
- issues_enabled
- jobs_enabled
@@ -164,6 +164,22 @@ project_setting:
- emails_enabled
- pages_unique_domain_enabled
- pages_unique_domain
+ - runner_registration_enabled
+ - product_analytics_instrumentation_key
+ - jitsu_host
+ - jitsu_project_xid
+ - jitsu_administrator_email
+ - jitsu_administrator_password
+ - encrypted_jitsu_administrator_password
+ - encrypted_jitsu_administrator_password_iv
+ - product_analytics_data_collector_host
+ - product_analytics_clickhouse_connection_string
+ - encrypted_product_analytics_clickhouse_connection_string
+ - encrypted_product_analytics_clickhouse_connection_string_iv
+ - cube_api_base_url
+ - cube_api_key
+ - encrypted_cube_api_key
+ - encrypted_cube_api_key_iv
build_service_desk_setting: # service_desk_setting
unexposed_attributes:
diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb
index 895192252da..c52948a4cb0 100644
--- a/spec/requests/api/project_clusters_spec.rb
+++ b/spec/requests/api/project_clusters_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ProjectClusters, feature_category: :kubernetes_management do
+RSpec.describe API::ProjectClusters, feature_category: :deployment_management do
include KubernetesHelpers
let_it_be(:maintainer_user) { create(:user) }
diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb
index 096f0b73b4c..22d7ea36f6c 100644
--- a/spec/requests/api/project_export_spec.rb
+++ b/spec/requests/api/project_export_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category: :importers do
+RSpec.describe API::ProjectExport, :aggregate_failures, :clean_gitlab_redis_cache, feature_category: :importers do
let_it_be(:project) { create(:project) }
let_it_be(:project_none) { create(:project) }
let_it_be(:project_started) { create(:project) }
@@ -45,21 +45,27 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
end
describe 'GET /projects/:project_id/export' do
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:failed_status_code) { :not_found }
+ end
+
shared_examples_for 'get project export status not found' do
it_behaves_like '404 response' do
- let(:request) { get api(path, user) }
+ subject(:request) { get api(path, user) }
end
end
shared_examples_for 'get project export status denied' do
it_behaves_like '403 response' do
- let(:request) { get api(path, user) }
+ subject(:request) { get api(path, user) }
end
end
shared_examples_for 'get project export status ok' do
+ let_it_be(:admin_mode) { false }
+
it 'is none' do
- get api(path_none, user)
+ get api(path_none, user, admin_mode: admin_mode)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/project/export_status')
@@ -72,7 +78,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
end
it 'returns status started' do
- get api(path_started, user)
+ get api(path_started, user, admin_mode: admin_mode)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/project/export_status')
@@ -82,7 +88,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
context 'when project export has finished' do
it 'returns status finished' do
- get api(path_finished, user)
+ get api(path_finished, user, admin_mode: admin_mode)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/project/export_status')
@@ -96,7 +102,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
end
it 'returns status regeneration_in_progress' do
- get api(path_finished, user)
+ get api(path_finished, user, admin_mode: admin_mode)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/project/export_status')
@@ -106,14 +112,16 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
end
it_behaves_like 'when project export is disabled' do
- let(:request) { get api(path, admin) }
+ subject(:request) { get api(path, admin, admin_mode: true) }
end
context 'when project export is enabled' do
context 'when user is an admin' do
let(:user) { admin }
- it_behaves_like 'get project export status ok'
+ it_behaves_like 'get project export status ok' do
+ let(:admin_mode) { true }
+ end
end
context 'when user is a maintainer' do
@@ -159,29 +167,34 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
end
describe 'GET /projects/:project_id/export/download' do
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:path) { download_path_finished }
+ let(:failed_status_code) { :not_found }
+ end
+
shared_examples_for 'get project export download not found' do
it_behaves_like '404 response' do
- let(:request) { get api(download_path, user) }
+ subject(:request) { get api(download_path, user) }
end
end
shared_examples_for 'get project export download denied' do
it_behaves_like '403 response' do
- let(:request) { get api(download_path, user) }
+ subject(:request) { get api(download_path, user) }
end
end
shared_examples_for 'get project export download' do
it_behaves_like '404 response' do
- let(:request) { get api(download_path_none, user) }
+ subject(:request) { get api(download_path_none, user, admin_mode: admin_mode) }
end
it_behaves_like '404 response' do
- let(:request) { get api(download_path_started, user) }
+ subject(:request) { get api(download_path_started, user, admin_mode: admin_mode) }
end
it 'downloads' do
- get api(download_path_finished, user)
+ get api(download_path_finished, user, admin_mode: admin_mode)
expect(response).to have_gitlab_http_status(:ok)
end
@@ -190,7 +203,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
shared_examples_for 'get project export upload after action' do
context 'and is uploading' do
it 'downloads' do
- get api(download_path_export_action, user)
+ get api(download_path_export_action, user, admin_mode: admin_mode)
expect(response).to have_gitlab_http_status(:ok)
end
@@ -202,7 +215,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
end
it 'returns 404' do
- get api(download_path_export_action, user)
+ get api(download_path_export_action, user, admin_mode: admin_mode)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('The project export file is not available yet')
@@ -219,12 +232,14 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
end
it_behaves_like '404 response' do
- let(:request) { get api(download_path_export_action, user) }
+ subject(:request) { get api(download_path_export_action, user, admin_mode: admin_mode) }
end
end
end
shared_examples_for 'get project download by strategy' do
+ let_it_be(:admin_mode) { false }
+
context 'when upload strategy set' do
it_behaves_like 'get project export upload after action'
end
@@ -235,17 +250,19 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
end
it_behaves_like 'when project export is disabled' do
- let(:request) { get api(download_path, admin) }
+ subject(:request) { get api(download_path, admin, admin_mode: true) }
end
context 'when project export is enabled' do
context 'when user is an admin' do
let(:user) { admin }
- it_behaves_like 'get project download by strategy'
+ it_behaves_like 'get project download by strategy' do
+ let(:admin_mode) { true }
+ end
context 'when rate limit is exceeded' do
- let(:request) { get api(download_path, admin) }
+ subject(:request) { get api(download_path, admin, admin_mode: true) }
before do
allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy|
@@ -271,7 +288,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
# simulate prior request to the same namespace, which increments the rate limit counter for that scope
Gitlab::ApplicationRateLimiter.throttled?(:project_download_export, scope: [user, project_finished.namespace])
- get api(download_path_finished, user)
+ get api(download_path_finished, user, admin_mode: true)
expect(response).to have_gitlab_http_status(:too_many_requests)
end
@@ -280,7 +297,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
Gitlab::ApplicationRateLimiter.throttled?(:project_download_export,
scope: [user, create(:project, :with_export).namespace])
- get api(download_path_finished, user)
+ get api(download_path_finished, user, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
end
end
@@ -345,30 +362,41 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
end
describe 'POST /projects/:project_id/export' do
+ let(:admin_mode) { false }
+ let(:params) { {} }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { { 'upload[url]' => 'http://gitlab.com' } }
+ let(:failed_status_code) { :not_found }
+ let(:success_status_code) { :accepted }
+ end
+
+ subject(:request) { post api(path, user, admin_mode: admin_mode), params: params }
+
shared_examples_for 'post project export start not found' do
- it_behaves_like '404 response' do
- let(:request) { post api(path, user) }
- end
+ it_behaves_like '404 response'
end
shared_examples_for 'post project export start denied' do
- it_behaves_like '403 response' do
- let(:request) { post api(path, user) }
- end
+ it_behaves_like '403 response'
end
shared_examples_for 'post project export start' do
+ let_it_be(:admin_mode) { false }
+
context 'with upload strategy' do
context 'when params invalid' do
it_behaves_like '400 response' do
- let(:request) { post(api(path, user), params: { 'upload[url]' => 'whatever' }) }
+ let(:params) { { 'upload[url]' => 'whatever' } }
end
end
it 'starts' do
allow_any_instance_of(Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy).to receive(:send_file)
- post(api(path, user), params: { 'upload[url]' => 'http://gitlab.com' })
+ request do
+ let(:params) { { 'upload[url]' => 'http://gitlab.com' } }
+ end
expect(response).to have_gitlab_http_status(:accepted)
end
@@ -388,7 +416,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
it 'starts' do
expect_any_instance_of(Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy).not_to receive(:send_file)
- post api(path, user)
+ request
expect(response).to have_gitlab_http_status(:accepted)
end
@@ -396,20 +424,21 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
it 'removes previously exported archive file' do
expect(project).to receive(:remove_exports).once
- post api(path, user)
+ request
end
end
end
- it_behaves_like 'when project export is disabled' do
- let(:request) { post api(path, admin) }
- end
+ it_behaves_like 'when project export is disabled'
context 'when project export is enabled' do
context 'when user is an admin' do
let(:user) { admin }
+ let(:admin_mode) { true }
- it_behaves_like 'post project export start'
+ it_behaves_like 'post project export start' do
+ let(:admin_mode) { true }
+ end
context 'with project export size limit' do
before do
@@ -417,7 +446,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
end
it 'starts if limit not exceeded' do
- post api(path, user)
+ request
expect(response).to have_gitlab_http_status(:accepted)
end
@@ -425,7 +454,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
it '400 response if limit exceeded' do
project.statistics.update!(lfs_objects_size: 2.megabytes, repository_size: 2.megabytes)
- post api(path, user)
+ request
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response["message"]).to include('The project size exceeds the export limit.')
@@ -441,7 +470,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
end
it 'prevents requesting project export' do
- post api(path, admin)
+ request
expect(response).to have_gitlab_http_status(:too_many_requests)
expect(json_response['message']['error']).to eq('This endpoint has been requested too many times. Try again later.')
@@ -559,7 +588,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
let(:relation) { ::BulkImports::FileTransfer::ProjectConfig.new(project).skipped_relations.first }
it_behaves_like '400 response' do
- let(:request) { get api(download_path, user) }
+ subject(:request) { get api(download_path, user) }
end
end
@@ -595,7 +624,7 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
describe 'POST /projects/:id/export_relations' do
it_behaves_like '404 response' do
- let(:request) { post api(path, user) }
+ subject(:request) { post api(path, user) }
end
end
@@ -608,13 +637,13 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
end
it_behaves_like '404 response' do
- let(:request) { post api(path, user) }
+ subject(:request) { post api(path, user) }
end
end
describe 'GET /projects/:id/export_relations/status' do
it_behaves_like '404 response' do
- let(:request) { get api(status_path, user) }
+ subject(:request) { get api(status_path, user) }
end
end
end
@@ -629,26 +658,26 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache, feature_category:
describe 'POST /projects/:id/export_relations' do
it_behaves_like '403 response' do
- let(:request) { post api(path, developer) }
+ subject(:request) { post api(path, developer) }
end
end
describe 'GET /projects/:id/export_relations/download' do
it_behaves_like '403 response' do
- let(:request) { get api(download_path, developer) }
+ subject(:request) { get api(download_path, developer) }
end
end
describe 'GET /projects/:id/export_relations/status' do
it_behaves_like '403 response' do
- let(:request) { get api(status_path, developer) }
+ subject(:request) { get api(status_path, developer) }
end
end
end
context 'when bulk import is disabled' do
it_behaves_like '404 response' do
- let(:request) { get api(path, user) }
+ subject(:request) { get api(path, user) }
end
end
end
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index 027c61bb9e1..4496e3aa7c3 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -14,6 +14,8 @@ RSpec.describe API::ProjectImport, :aggregate_failures, feature_category: :impor
before do
namespace.add_owner(user) if user
+
+ stub_application_setting(import_sources: ['gitlab_project'])
end
shared_examples 'requires authentication' do
@@ -26,6 +28,20 @@ RSpec.describe API::ProjectImport, :aggregate_failures, feature_category: :impor
end
end
+ shared_examples 'requires import source to be enabled' do
+ context 'when gitlab_project import_sources is disabled' do
+ before do
+ stub_application_setting(import_sources: [])
+ end
+
+ it 'returns 403' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
describe 'POST /projects/import' do
subject { upload_archive(file_upload, workhorse_headers, params) }
@@ -43,6 +59,7 @@ RSpec.describe API::ProjectImport, :aggregate_failures, feature_category: :impor
end
it_behaves_like 'requires authentication'
+ it_behaves_like 'requires import source to be enabled'
it 'executes a limited number of queries', :use_clean_rails_redis_caching do
control_count = ActiveRecord::QueryRecorder.new { subject }.count
@@ -247,7 +264,7 @@ RSpec.describe API::ProjectImport, :aggregate_failures, feature_category: :impor
subject
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to eq('Project namespace name has already been taken')
+ expect(json_response['message']).to eq('Path has already been taken')
end
context 'when param overwrite is true' do
@@ -337,6 +354,7 @@ RSpec.describe API::ProjectImport, :aggregate_failures, feature_category: :impor
end
it_behaves_like 'requires authentication'
+ it_behaves_like 'requires import source to be enabled'
context 'when the response is successful' do
it 'schedules the import successfully' do
@@ -402,64 +420,51 @@ RSpec.describe API::ProjectImport, :aggregate_failures, feature_category: :impor
end
it_behaves_like 'requires authentication'
+ it_behaves_like 'requires import source to be enabled'
- it 'returns NOT FOUND when the feature is disabled' do
- stub_feature_flags(import_project_from_remote_file_s3: false)
-
- subject
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
-
- context 'when the feature flag is enabled' do
- before do
- stub_feature_flags(import_project_from_remote_file_s3: true)
- end
-
- context 'when the response is successful' do
- it 'schedules the import successfully' do
- project = create(
- :project,
- namespace: user.namespace,
- name: 'test-import',
- path: 'test-import'
- )
+ context 'when the response is successful' do
+ it 'schedules the import successfully' do
+ project = create(
+ :project,
+ namespace: user.namespace,
+ name: 'test-import',
+ path: 'test-import'
+ )
- service_response = ServiceResponse.success(payload: project)
- expect_next(::Import::GitlabProjects::CreateProjectService)
- .to receive(:execute)
- .and_return(service_response)
+ service_response = ServiceResponse.success(payload: project)
+ expect_next(::Import::GitlabProjects::CreateProjectService)
+ .to receive(:execute)
+ .and_return(service_response)
- subject
+ subject
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response).to include({
- 'id' => project.id,
- 'name' => 'test-import',
- 'name_with_namespace' => "#{user.namespace.name} / test-import",
- 'path' => 'test-import',
- 'path_with_namespace' => "#{user.namespace.path}/test-import"
- })
- end
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to include({
+ 'id' => project.id,
+ 'name' => 'test-import',
+ 'name_with_namespace' => "#{user.namespace.name} / test-import",
+ 'path' => 'test-import',
+ 'path_with_namespace' => "#{user.namespace.path}/test-import"
+ })
end
+ end
- context 'when the service returns an error' do
- it 'fails to schedule the import' do
- service_response = ServiceResponse.error(
- message: 'Failed to import',
- http_status: :bad_request
- )
- expect_next(::Import::GitlabProjects::CreateProjectService)
- .to receive(:execute)
- .and_return(service_response)
+ context 'when the service returns an error' do
+ it 'fails to schedule the import' do
+ service_response = ServiceResponse.error(
+ message: 'Failed to import',
+ http_status: :bad_request
+ )
+ expect_next(::Import::GitlabProjects::CreateProjectService)
+ .to receive(:execute)
+ .and_return(service_response)
- subject
+ subject
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response).to eq({
- 'message' => 'Failed to import'
- })
- end
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response).to eq({
+ 'message' => 'Failed to import'
+ })
end
end
end
@@ -510,6 +515,7 @@ RSpec.describe API::ProjectImport, :aggregate_failures, feature_category: :impor
subject { post api('/projects/import/authorize', user), headers: workhorse_headers }
it_behaves_like 'requires authentication'
+ it_behaves_like 'requires import source to be enabled'
it 'authorizes importing project with workhorse header' do
subject
diff --git a/spec/requests/api/project_job_token_scope_spec.rb b/spec/requests/api/project_job_token_scope_spec.rb
new file mode 100644
index 00000000000..df210a00012
--- /dev/null
+++ b/spec/requests/api/project_job_token_scope_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::ProjectJobTokenScope, feature_category: :secrets_management do
+ describe 'GET /projects/:id/job_token_scope' do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:user) { create(:user) }
+
+ let(:get_job_token_scope_path) { "/projects/#{project.id}/job_token_scope" }
+
+ subject { get api(get_job_token_scope_path, user) }
+
+ context 'when unauthenticated user (missing user)' do
+ context 'for public project' do
+ it 'does not return ci cd settings of job token' do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+
+ get api(get_job_token_scope_path)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+
+ context 'when authenticated user as maintainer' do
+ before_all { project.add_maintainer(user) }
+
+ it 'returns ci cd settings for job token scope' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ "inbound_enabled" => true,
+ "outbound_enabled" => false
+ )
+ end
+
+ it 'returns the correct ci cd settings for job token scope after change' do
+ project.update!(ci_inbound_job_token_scope_enabled: false)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ "inbound_enabled" => false,
+ "outbound_enabled" => false
+ )
+ end
+
+ it 'returns unauthorized and blank response when invalid auth credentials are given' do
+ invalid_personal_access_token = build(:personal_access_token, user: user)
+
+ get api(get_job_token_scope_path, user, personal_access_token: invalid_personal_access_token)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(json_response).not_to include("inbound_enabled", "outbound_enabled")
+ end
+ end
+
+ context 'when authenticated user as developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'returns forbidden and no ci cd settings for public project' do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(json_response).not_to include("inbound_enabled", "outbound_enabled")
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb
index 9d722e4a445..978ac28ef73 100644
--- a/spec/requests/api/project_milestones_spec.rb
+++ b/spec/requests/api/project_milestones_spec.rb
@@ -6,8 +6,12 @@ RSpec.describe API::ProjectMilestones, feature_category: :team_planning do
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:project) { create(:project, namespace: user.namespace) }
let_it_be(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') }
- let_it_be(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') }
let_it_be(:route) { "/projects/#{project.id}/milestones" }
+ let_it_be(:milestone) do
+ create(:milestone, project: project, title: 'version2', description: 'open milestone', updated_at: 5.days.ago)
+ end
+
+ let(:params) { {} }
before_all do
project.add_reporter(user)
@@ -15,38 +19,43 @@ RSpec.describe API::ProjectMilestones, feature_category: :team_planning do
it_behaves_like 'group and project milestones', "/projects/:id/milestones"
+ shared_examples 'listing all milestones' do
+ it 'returns correct list of milestones' do
+ get api(route, user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.size).to eq(milestones.size)
+ expect(json_response.map { |entry| entry["id"] }).to match_array(milestones.map(&:id))
+ end
+ end
+
describe 'GET /projects/:id/milestones' do
- context 'when include_parent_milestones is true' do
- let_it_be(:ancestor_group) { create(:group, :private) }
- let_it_be(:group) { create(:group, :private, parent: ancestor_group) }
- let_it_be(:ancestor_group_milestone) { create(:milestone, group: ancestor_group) }
- let_it_be(:group_milestone) { create(:milestone, group: group) }
+ let_it_be(:ancestor_group) { create(:group, :private) }
+ let_it_be(:group) { create(:group, :private, parent: ancestor_group) }
+ let_it_be(:ancestor_group_milestone) { create(:milestone, group: ancestor_group, updated_at: 1.day.ago) }
+ let_it_be(:group_milestone) { create(:milestone, group: group, updated_at: 3.days.ago) }
- let(:params) { { include_parent_milestones: true } }
+ context 'when project parent is a namespace' do
+ let(:milestones) { [milestone, closed_milestone] }
- shared_examples 'listing all milestones' do
- it 'returns correct list of milestones' do
- get api(route, user), params: params
+ it_behaves_like 'listing all milestones'
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to eq(milestones.size)
- expect(json_response.map { |entry| entry["id"] }).to eq(milestones.map(&:id))
- end
+ context 'when include_parent_milestones is true' do
+ let(:params) { { include_parent_milestones: true } }
+
+ it_behaves_like 'listing all milestones'
end
+ end
- context 'when project parent is a namespace' do
- it_behaves_like 'listing all milestones' do
- let(:milestones) { [milestone, closed_milestone] }
- end
+ context 'when project parent is a group' do
+ before_all do
+ project.update!(namespace: group)
end
- context 'when project parent is a group' do
+ context 'when include_parent_milestones is true' do
+ let(:params) { { include_parent_milestones: true } }
let(:milestones) { [group_milestone, ancestor_group_milestone, milestone, closed_milestone] }
- before_all do
- project.update!(namespace: group)
- end
-
it_behaves_like 'listing all milestones'
context 'when iids param is present' do
@@ -64,6 +73,38 @@ RSpec.describe API::ProjectMilestones, feature_category: :team_planning do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'when updated_before param is present' do
+ let(:params) { { updated_before: 12.hours.ago.iso8601, include_parent_milestones: true } }
+
+ it_behaves_like 'listing all milestones' do
+ let(:milestones) { [group_milestone, ancestor_group_milestone, milestone] }
+ end
+ end
+
+ context 'when updated_after param is present' do
+ let(:params) { { updated_after: 2.days.ago.iso8601, include_parent_milestones: true } }
+
+ it_behaves_like 'listing all milestones' do
+ let(:milestones) { [ancestor_group_milestone, closed_milestone] }
+ end
+ end
+ end
+
+ context 'when updated_before param is present' do
+ let(:params) { { updated_before: 12.hours.ago.iso8601 } }
+
+ it_behaves_like 'listing all milestones' do
+ let(:milestones) { [milestone] }
+ end
+ end
+
+ context 'when updated_after param is present' do
+ let(:params) { { updated_after: 2.days.ago.iso8601 } }
+
+ it_behaves_like 'listing all milestones' do
+ let(:milestones) { [closed_milestone] }
+ end
end
end
end
diff --git a/spec/requests/api/project_snapshots_spec.rb b/spec/requests/api/project_snapshots_spec.rb
index 5d3c596e605..cbf6907f9a3 100644
--- a/spec/requests/api/project_snapshots_spec.rb
+++ b/spec/requests/api/project_snapshots_spec.rb
@@ -2,11 +2,12 @@
require 'spec_helper'
-RSpec.describe API::ProjectSnapshots, feature_category: :source_code_management do
+RSpec.describe API::ProjectSnapshots, :aggregate_failures, feature_category: :source_code_management do
include WorkhorseHelpers
let(:project) { create(:project) }
let(:admin) { create(:admin) }
+ let(:path) { "/projects/#{project.id}/snapshot" }
before do
allow(Feature::Gitaly).to receive(:server_feature_flags).and_return({
@@ -32,27 +33,29 @@ RSpec.describe API::ProjectSnapshots, feature_category: :source_code_management
expect(response.parsed_body).to be_empty
end
+ it_behaves_like 'GET request permissions for admin mode'
+
it 'returns authentication error as project owner' do
- get api("/projects/#{project.id}/snapshot", project.first_owner)
+ get api(path, project.first_owner)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'returns authentication error as unauthenticated user' do
- get api("/projects/#{project.id}/snapshot", nil)
+ get api(path, nil)
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'requests project repository raw archive as administrator' do
- get api("/projects/#{project.id}/snapshot", admin), params: { wiki: '0' }
+ get api(path, admin, admin_mode: true), params: { wiki: '0' }
expect(response).to have_gitlab_http_status(:ok)
expect_snapshot_response_for(project.repository)
end
it 'requests wiki repository raw archive as administrator' do
- get api("/projects/#{project.id}/snapshot", admin), params: { wiki: '1' }
+ get api(path, admin, admin_mode: true), params: { wiki: '1' }
expect(response).to have_gitlab_http_status(:ok)
expect_snapshot_response_for(project.wiki.repository)
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 267557b8137..f0aa61c688b 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::ProjectSnippets, feature_category: :source_code_management do
+RSpec.describe API::ProjectSnippets, :aggregate_failures, feature_category: :source_code_management do
include SnippetHelpers
let_it_be(:project) { create(:project, :public) }
@@ -14,8 +14,12 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
describe "GET /projects/:project_id/snippets/:id/user_agent_detail" do
let_it_be(:user_agent_detail) { create(:user_agent_detail, subject: public_snippet) }
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:path) { "/projects/#{public_snippet.project.id}/snippets/#{public_snippet.id}/user_agent_detail" }
+ end
+
it 'exposes known attributes' do
- get api("/projects/#{project.id}/snippets/#{public_snippet.id}/user_agent_detail", admin)
+ get api("/projects/#{project.id}/snippets/#{public_snippet.id}/user_agent_detail", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['user_agent']).to eq(user_agent_detail.user_agent)
@@ -26,7 +30,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
it 'respects project scoping' do
other_project = create(:project)
- get api("/projects/#{other_project.id}/snippets/#{public_snippet.id}/user_agent_detail", admin)
+ get api("/projects/#{other_project.id}/snippets/#{public_snippet.id}/user_agent_detail", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -38,7 +42,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
context 'with snippets disabled' do
it_behaves_like '403 response' do
- let(:request) { get api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}/user_agent_detail", admin) }
+ subject(:request) { get api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}/user_agent_detail", admin, admin_mode: true) }
end
end
end
@@ -72,7 +76,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
context 'with snippets disabled' do
it_behaves_like '403 response' do
- let(:request) { get api("/projects/#{project_no_snippets.id}/snippets", user) }
+ subject(:request) { get api("/projects/#{project_no_snippets.id}/snippets", user) }
end
end
end
@@ -83,16 +87,14 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
it 'returns snippet json' do
get api("/projects/#{project.id}/snippets/#{snippet.id}", user)
- aggregate_failures do
- expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['title']).to eq(snippet.title)
- expect(json_response['description']).to eq(snippet.description)
- expect(json_response['file_name']).to eq(snippet.file_name_on_repo)
- expect(json_response['files']).to eq(snippet.blobs.map { |blob| snippet_blob_file(blob) })
- expect(json_response['ssh_url_to_repo']).to eq(snippet.ssh_url_to_repo)
- expect(json_response['http_url_to_repo']).to eq(snippet.http_url_to_repo)
- end
+ expect(json_response['title']).to eq(snippet.title)
+ expect(json_response['description']).to eq(snippet.description)
+ expect(json_response['file_name']).to eq(snippet.file_name_on_repo)
+ expect(json_response['files']).to eq(snippet.blobs.map { |blob| snippet_blob_file(blob) })
+ expect(json_response['ssh_url_to_repo']).to eq(snippet.ssh_url_to_repo)
+ expect(json_response['http_url_to_repo']).to eq(snippet.http_url_to_repo)
end
it 'returns 404 for invalid snippet id' do
@@ -104,7 +106,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
context 'with snippets disabled' do
it_behaves_like '403 response' do
- let(:request) { get api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}", user) }
+ subject(:request) { get api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}", user) }
end
end
@@ -126,22 +128,25 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
let(:file_content) { 'puts "hello world"' }
let(:file_params) { { files: [{ file_path: file_path, content: file_content }] } }
let(:params) { base_params.merge(file_params) }
+ let(:admin_mode) { false }
+
+ subject(:request) { post api("/projects/#{project.id}/snippets/", actor, admin_mode: admin_mode), params: params }
- subject { post api("/projects/#{project.id}/snippets/", actor), params: params }
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:path) { "/projects/#{project.id}/snippets/" }
+ end
shared_examples 'project snippet repository actions' do
let(:snippet) { ProjectSnippet.find(json_response['id']) }
it 'commit the files to the repository' do
- subject
+ request
- aggregate_failures do
- expect(snippet.repository.exists?).to be_truthy
+ expect(snippet.repository.exists?).to be_truthy
- blob = snippet.repository.blob_at(snippet.default_branch, file_path)
+ blob = snippet.repository.blob_at(snippet.default_branch, file_path)
- expect(blob.data).to eq file_content
- end
+ expect(blob.data).to eq file_content
end
end
@@ -152,7 +157,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
it 'creates a new snippet' do
project.add_developer(actor)
- subject
+ request
expect(response).to have_gitlab_http_status(:created)
end
@@ -160,7 +165,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
context 'that does not belong to the project' do
it 'does not create a new snippet' do
- subject
+ request
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -180,7 +185,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
end
it 'creates a new snippet' do
- subject
+ request
expect(response).to have_gitlab_http_status(:created)
snippet = ProjectSnippet.find(json_response['id'])
@@ -196,9 +201,10 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
context 'with an admin' do
let(:actor) { admin }
+ let(:admin_mode) { true }
it 'creates a new snippet' do
- subject
+ request
expect(response).to have_gitlab_http_status(:created)
snippet = ProjectSnippet.find(json_response['id'])
@@ -214,7 +220,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
it 'returns 400 for missing parameters' do
params.delete(:title)
- subject
+ request
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -226,7 +232,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
it 'returns 400 if title is blank' do
params[:title] = ''
- subject
+ request
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq 'title is empty'
@@ -235,6 +241,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
context 'when save fails because the repository could not be created' do
let(:actor) { admin }
+ let(:admin_mode) { true }
before do
allow_next_instance_of(Snippets::CreateService) do |instance|
@@ -243,7 +250,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
end
it 'returns 400' do
- subject
+ request
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -264,7 +271,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
it 'creates the snippet' do
params['visibility'] = 'private'
- expect { subject }.to change { Snippet.count }.by(1)
+ expect { request }.to change { Snippet.count }.by(1)
end
end
@@ -274,13 +281,13 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
end
it 'rejects the snippet' do
- expect { subject }.not_to change { Snippet.count }
+ expect { request }.not_to change { Snippet.count }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['error']).to match(/snippet has been recognized as spam/)
end
it 'creates a spam log' do
- expect { subject }
+ expect { request }
.to log_spam(title: 'Test Title', user_id: user.id, noteable_type: 'ProjectSnippet')
end
end
@@ -288,7 +295,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
context 'with snippets disabled' do
it_behaves_like '403 response' do
- let(:request) { post api("/projects/#{project_no_snippets.id}/snippets", user), params: params }
+ subject(:request) { post api("/projects/#{project_no_snippets.id}/snippets", user), params: params }
end
end
end
@@ -296,6 +303,11 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
describe 'PUT /projects/:project_id/snippets/:id/' do
let(:visibility_level) { Snippet::PUBLIC }
let(:snippet) { create(:project_snippet, :repository, author: admin, visibility_level: visibility_level, project: project) }
+ let(:params) { { title: 'Foo' } }
+
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:path) { "/projects/#{snippet.project.id}/snippets/#{snippet.id}" }
+ end
it_behaves_like 'snippet file updates'
it_behaves_like 'snippet non-file updates'
@@ -317,7 +329,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
let(:visibility_level) { Snippet::PRIVATE }
it 'creates the snippet' do
- expect { update_snippet(params: { title: 'Foo' }) }
+ expect { update_snippet(admin_mode: true, params: params) }
.to change { snippet.reload.title }.to('Foo')
end
end
@@ -326,12 +338,12 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
let(:visibility_level) { Snippet::PUBLIC }
it 'rejects the snippet' do
- expect { update_snippet(params: { title: 'Foo' }) }
+ expect { update_snippet(params: params) }
.not_to change { snippet.reload.title }
end
it 'creates a spam log' do
- expect { update_snippet(params: { title: 'Foo' }) }
+ expect { update_snippet(params: params) }
.to log_spam(title: 'Foo', user_id: admin.id, noteable_type: 'ProjectSnippet')
end
end
@@ -340,7 +352,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
let(:visibility_level) { Snippet::PRIVATE }
it 'rejects the snippet' do
- expect { update_snippet(params: { title: 'Foo', visibility: 'public' }) }
+ expect { update_snippet(admin_mode: true, params: { title: 'Foo', visibility: 'public' }) }
.not_to change { snippet.reload.title }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -348,7 +360,7 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
end
it 'creates a spam log' do
- expect { update_snippet(params: { title: 'Foo', visibility: 'public' }) }
+ expect { update_snippet(admin_mode: true, params: { title: 'Foo', visibility: 'public' }) }
.to log_spam(title: 'Foo', user_id: admin.id, noteable_type: 'ProjectSnippet')
end
end
@@ -356,47 +368,58 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
context 'with snippets disabled' do
it_behaves_like '403 response' do
- let(:request) { put api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}", admin), params: { description: 'foo' } }
+ subject(:request) { put api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}", admin, admin_mode: true), params: { description: 'foo' } }
end
end
- def update_snippet(snippet_id: snippet.id, params: {})
- put api("/projects/#{snippet.project.id}/snippets/#{snippet_id}", admin), params: params
+ def update_snippet(snippet_id: snippet.id, admin_mode: false, params: {})
+ put api("/projects/#{snippet.project.id}/snippets/#{snippet_id}", admin, admin_mode: admin_mode), params: params
end
end
describe 'DELETE /projects/:project_id/snippets/:id/' do
let_it_be(:snippet, refind: true) { public_snippet }
+ let(:path) { "/projects/#{snippet.project.id}/snippets/#{snippet.id}/" }
+
+ it_behaves_like 'DELETE request permissions for admin mode'
it 'deletes snippet' do
- delete api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin)
+ delete api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
end
it 'returns 404 for invalid snippet id' do
- delete api("/projects/#{snippet.project.id}/snippets/#{non_existing_record_id}", admin)
+ delete api("/projects/#{snippet.project.id}/snippets/#{non_existing_record_id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
it_behaves_like '412 response' do
- let(:request) { api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin) }
+ subject(:request) { api(path, admin, admin_mode: true) }
end
context 'with snippets disabled' do
it_behaves_like '403 response' do
- let(:request) { delete api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}", admin) }
+ subject(:request) { delete api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}", admin, admin_mode: true) }
end
end
end
describe 'GET /projects/:project_id/snippets/:id/raw' do
let_it_be(:snippet) { create(:project_snippet, :repository, :public, author: admin, project: project) }
+ let(:path) { "/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw" }
+
+ it_behaves_like 'GET request permissions for admin mode' do
+ let_it_be(:snippet_with_empty_repo) { create(:project_snippet, :empty_repo, author: admin, project: project) }
+
+ let(:snippet) { snippet_with_empty_repo }
+ let(:failed_status_code) { :not_found }
+ end
it 'returns raw text' do
- get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin)
+ get api(path, admin)
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq 'text/plain'
@@ -404,38 +427,41 @@ RSpec.describe API::ProjectSnippets, feature_category: :source_code_management d
end
it 'returns 404 for invalid snippet id' do
- get api("/projects/#{snippet.project.id}/snippets/#{non_existing_record_id}/raw", admin)
+ get api("/projects/#{snippet.project.id}/snippets/#{non_existing_record_id}/raw", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
- it_behaves_like 'project snippet access levels' do
- let(:path) { "/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw" }
- end
+ it_behaves_like 'project snippet access levels'
context 'with snippets disabled' do
it_behaves_like '403 response' do
- let(:request) { get api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}/raw", admin) }
+ subject(:request) { get api("/projects/#{project_no_snippets.id}/snippets/#{non_existing_record_id}/raw", admin, admin_mode: true) }
end
end
it_behaves_like 'snippet blob content' do
let_it_be(:snippet_with_empty_repo) { create(:project_snippet, :empty_repo, author: admin, project: project) }
+ let_it_be(:admin_mode) { snippet.author.admin? }
- subject { get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", snippet.author) }
+ subject { get api(path, snippet.author, admin_mode: admin_mode) }
end
end
describe 'GET /projects/:project_id/snippets/:id/files/:ref/:file_path/raw' do
let_it_be(:snippet) { create(:project_snippet, :repository, author: admin, project: project) }
+ let(:path) { "/projects/#{snippet.project.id}/snippets/#{snippet.id}/files/master/%2Egitattributes/raw" }
+
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:failed_status_code) { :not_found }
+ end
+
it_behaves_like 'raw snippet files' do
let(:api_path) { "/projects/#{snippet.project.id}/snippets/#{snippet_id}/files/#{ref}/#{file_path}/raw" }
end
- it_behaves_like 'project snippet access levels' do
- let(:path) { "/projects/#{snippet.project.id}/snippets/#{snippet.id}/files/master/%2Egitattributes/raw" }
- end
+ it_behaves_like 'project snippet access levels'
end
end
diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb
index 38d6a05a104..91e5ed76c37 100644
--- a/spec/requests/api/project_templates_spec.rb
+++ b/spec/requests/api/project_templates_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe API::ProjectTemplates, feature_category: :source_code_management
let(:url_encoded_path) { "#{public_project.namespace.path}%2F#{public_project.path}" }
before do
+ stub_feature_flags(remove_monitor_metrics: false)
private_project.add_developer(developer)
end
@@ -71,6 +72,18 @@ RSpec.describe API::ProjectTemplates, feature_category: :source_code_management
expect(json_response).to satisfy_one { |template| template['key'] == 'Default' }
end
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'returns 400 bad request like other unknown types' do
+ get api("/projects/#{public_project.id}/templates/metrics_dashboard_ymls")
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
it 'returns issue templates' do
get api("/projects/#{private_project.id}/templates/issues", developer)
@@ -171,6 +184,18 @@ RSpec.describe API::ProjectTemplates, feature_category: :source_code_management
expect(json_response['name']).to eq('Default')
end
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'returns 400 bad request like other unknown types' do
+ get api("/projects/#{public_project.id}/templates/metrics_dashboard_ymls/Default")
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
it 'returns a specific license' do
get api("/projects/#{public_project.id}/templates/licenses/mit")
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index e78ef2f7630..349101a092f 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -15,7 +15,7 @@ RSpec.shared_examples 'languages and percentages JSON response' do
end
context "when the languages haven't been detected yet" do
- it 'returns expected language values', :sidekiq_might_not_need_inline do
+ it 'returns expected language values', :aggregate_failures, :sidekiq_might_not_need_inline do
get api("/projects/#{project.id}/languages", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -33,7 +33,7 @@ RSpec.shared_examples 'languages and percentages JSON response' do
Projects::DetectRepositoryLanguagesService.new(project, project.first_owner).execute
end
- it 'returns the detection from the database' do
+ it 'returns the detection from the database', :aggregate_failures do
# Allow this to happen once, so the expected languages can be determined
expect(project.repository).to receive(:languages).once
@@ -46,7 +46,7 @@ RSpec.shared_examples 'languages and percentages JSON response' do
end
end
-RSpec.describe API::Projects, feature_category: :projects do
+RSpec.describe API::Projects, :aggregate_failures, feature_category: :projects do
include ProjectForksHelper
include WorkhorseHelpers
include StubRequests
@@ -55,16 +55,14 @@ RSpec.describe API::Projects, feature_category: :projects do
let_it_be(:user2) { create(:user) }
let_it_be(:user3) { create(:user) }
let_it_be(:admin) { create(:admin) }
- let_it_be(:project, reload: true) { create(:project, :repository, create_branch: 'something_else', namespace: user.namespace) }
- let_it_be(:project2, reload: true) { create(:project, namespace: user.namespace) }
+ let_it_be(:project, reload: true) { create(:project, :repository, create_branch: 'something_else', namespace: user.namespace, updated_at: 5.days.ago) }
+ let_it_be(:project2, reload: true) { create(:project, namespace: user.namespace, updated_at: 4.days.ago) }
let_it_be(:project_member) { create(:project_member, :developer, user: user3, project: project) }
let_it_be(:user4) { create(:user, username: 'user.withdot') }
let_it_be(:project3, reload: true) do
create(:project,
:private,
:repository,
- name: 'second_project',
- path: 'second_project',
creator_id: user.id,
namespace: user.namespace,
merge_requests_enabled: false,
@@ -82,8 +80,6 @@ RSpec.describe API::Projects, feature_category: :projects do
let_it_be(:project4, reload: true) do
create(:project,
- name: 'third_project',
- path: 'third_project',
creator_id: user4.id,
namespace: user4.namespace)
end
@@ -149,9 +145,15 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'GET /projects' do
+ let(:path) { '/projects' }
+
+ let_it_be(:public_project) { create(:project, :public, name: 'public_project') }
+
shared_examples_for 'projects response' do
+ let_it_be(:admin_mode) { false }
+
it 'returns an array of projects' do
- get api('/projects', current_user), params: filter
+ get api(path, current_user, admin_mode: admin_mode), params: filter
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -160,7 +162,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'returns the proper security headers' do
- get api('/projects', current_user), params: filter
+ get api(path, current_user, admin_mode: admin_mode), params: filter
expect(response).to include_security_headers
end
@@ -171,22 +173,20 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'avoids N + 1 queries', :use_sql_query_cache do
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api('/projects', current_user)
+ get api(path, current_user)
end
additional_project
expect do
- get api('/projects', current_user)
+ get api(path, current_user)
end.not_to exceed_all_query_limit(control).with_threshold(threshold)
end
end
- let_it_be(:public_project) { create(:project, :public, name: 'public_project') }
-
context 'when unauthenticated' do
it_behaves_like 'projects response' do
- let(:filter) { { search: project.name } }
+ let(:filter) { { search: project.path } }
let(:current_user) { user }
let(:projects) { [project] }
end
@@ -208,10 +208,10 @@ RSpec.describe API::Projects, feature_category: :projects do
end
shared_examples 'includes container_registry_access_level' do
- it do
+ specify do
project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED)
- get api('/projects', user)
+ get api(path, user)
project_response = json_response.find { |p| p['id'] == project.id }
expect(response).to have_gitlab_http_status(:ok)
@@ -231,8 +231,8 @@ RSpec.describe API::Projects, feature_category: :projects do
include_examples 'includes container_registry_access_level'
end
- it 'includes various project feature fields', :aggregate_failures do
- get api('/projects', user)
+ it 'includes various project feature fields' do
+ get api(path, user)
project_response = json_response.find { |p| p['id'] == project.id }
expect(response).to have_gitlab_http_status(:ok)
@@ -254,10 +254,10 @@ RSpec.describe API::Projects, feature_category: :projects do
end
end
- it 'includes correct value of container_registry_enabled', :aggregate_failures do
+ it 'includes correct value of container_registry_enabled' do
project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED)
- get api('/projects', user)
+ get api(path, user)
project_response = json_response.find { |p| p['id'] == project.id }
expect(response).to have_gitlab_http_status(:ok)
@@ -266,7 +266,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'includes project topics' do
- get api('/projects', user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -276,7 +276,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'includes open_issues_count' do
- get api('/projects', user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -287,7 +287,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'does not include projects marked for deletion' do
project.update!(pending_delete: true)
- get api('/projects', user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
@@ -297,7 +297,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'does not include open_issues_count if issues are disabled' do
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
- get api('/projects', user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -311,7 +311,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'returns no projects' do
- get api('/projects', user), params: { topic: 'foo' }
+ get api(path, user), params: { topic: 'foo' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -319,7 +319,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'returns matching project for a single topic' do
- get api('/projects', user), params: { topic: 'ruby' }
+ get api(path, user), params: { topic: 'ruby' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -327,7 +327,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'returns matching project for multiple topics' do
- get api('/projects', user), params: { topic: 'ruby, javascript' }
+ get api(path, user), params: { topic: 'ruby, javascript' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -335,7 +335,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'returns no projects if project match only some topic' do
- get api('/projects', user), params: { topic: 'ruby, foo' }
+ get api(path, user), params: { topic: 'ruby, foo' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -343,7 +343,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'ignores topic if it is empty' do
- get api('/projects', user), params: { topic: '' }
+ get api(path, user), params: { topic: '' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -404,7 +404,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it "does not include statistics by default" do
- get api('/projects', user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -413,7 +413,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it "includes statistics if requested" do
- get api('/projects', user), params: { statistics: true }
+ get api(path, user), params: { statistics: true }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -425,7 +425,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it "does not include license by default" do
- get api('/projects', user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -434,7 +434,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it "does not include license if requested" do
- get api('/projects', user), params: { license: true }
+ get api(path, user), params: { license: true }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -446,7 +446,7 @@ RSpec.describe API::Projects, feature_category: :projects do
let!(:jira_integration) { create(:jira_integration, project: project) }
it 'includes open_issues_count' do
- get api('/projects', user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -458,7 +458,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'does not include open_issues_count if issues are disabled' do
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
- get api('/projects', user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -501,7 +501,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'returns every project' do
- get api('/projects', user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -510,9 +510,38 @@ RSpec.describe API::Projects, feature_category: :projects do
end
end
+ context 'filter by updated_at' do
+ let(:filter) { { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago, order_by: :updated_at } }
+
+ it_behaves_like 'projects response' do
+ let(:current_user) { user }
+ let(:projects) { [project2, project] }
+ end
+
+ it 'returns projects sorted by updated_at' do
+ get api(path, user), params: filter
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.map { |p| p['id'] }).to match([project2, project].map(&:id))
+ end
+
+ context 'when filtering by updated_at and sorting by a different column' do
+ let(:filter) { { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago, order_by: 'id' } }
+
+ it 'returns an error' do
+ get api(path, user), params: filter
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq(
+ '400 Bad request - `updated_at` filter and `updated_at` sorting must be paired'
+ )
+ end
+ end
+ end
+
context 'and using search' do
it_behaves_like 'projects response' do
- let(:filter) { { search: project.name } }
+ let(:filter) { { search: project.path } }
let(:current_user) { user }
let(:projects) { [project] }
end
@@ -583,7 +612,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'and using the visibility filter' do
it 'filters based on private visibility param' do
- get api('/projects', user), params: { visibility: 'private' }
+ get api(path, user), params: { visibility: 'private' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -594,7 +623,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'filters based on internal visibility param' do
project2.update_attribute(:visibility_level, Gitlab::VisibilityLevel::INTERNAL)
- get api('/projects', user), params: { visibility: 'internal' }
+ get api(path, user), params: { visibility: 'internal' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -603,7 +632,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'filters based on public visibility param' do
- get api('/projects', user), params: { visibility: 'public' }
+ get api(path, user), params: { visibility: 'public' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -616,7 +645,7 @@ RSpec.describe API::Projects, feature_category: :projects do
include_context 'with language detection'
it 'filters case-insensitively by programming language' do
- get api('/projects', user), params: { with_programming_language: 'javascript' }
+ get api(path, user), params: { with_programming_language: 'javascript' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -627,7 +656,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'and using sorting' do
it 'returns the correct order when sorted by id' do
- get api('/projects', user), params: { order_by: 'id', sort: 'desc' }
+ get api(path, user), params: { order_by: 'id', sort: 'desc' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -638,7 +667,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'and with owned=true' do
it 'returns an array of projects the user owns' do
- get api('/projects', user4), params: { owned: true }
+ get api(path, user4), params: { owned: true }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -659,7 +688,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'does not list as owned project for admin' do
- get api('/projects', admin), params: { owned: true }
+ get api(path, admin, admin_mode: true), params: { owned: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_empty
@@ -675,7 +704,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'returns the starred projects viewable by the user' do
- get api('/projects', user3), params: { starred: true }
+ get api(path, user3), params: { starred: true }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -697,7 +726,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'including owned filter' do
it 'returns only projects that satisfy all query parameters' do
- get api('/projects', user), params: { visibility: 'public', owned: true, starred: true, search: 'gitlab' }
+ get api(path, user), params: { visibility: 'public', owned: true, starred: true, search: 'gitlab' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -716,7 +745,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'returns only projects that satisfy all query parameters' do
- get api('/projects', user), params: { visibility: 'public', membership: true, starred: true, search: 'gitlab' }
+ get api(path, user), params: { visibility: 'public', membership: true, starred: true, search: 'gitlab' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -735,7 +764,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'returns an array of projects the user has at least developer access' do
- get api('/projects', user2), params: { min_access_level: 30 }
+ get api(path, user2), params: { min_access_level: 30 }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -797,6 +826,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it_behaves_like 'projects response' do
let(:filter) { {} }
let(:current_user) { admin }
+ let(:admin_mode) { true }
let(:projects) { Project.all }
end
end
@@ -810,7 +840,7 @@ RSpec.describe API::Projects, feature_category: :projects do
let(:current_user) { user }
let(:params) { {} }
- subject { get api('/projects', current_user), params: params }
+ subject(:request) { get api(path, current_user), params: params }
before do
group_with_projects.add_owner(current_user) if current_user
@@ -818,7 +848,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'orders by id desc instead' do
projects_ordered_by_id_desc = /SELECT "projects".+ORDER BY "projects"."id" DESC/i
- expect { subject }.to make_queries_matching projects_ordered_by_id_desc
+ expect { request }.to make_queries_matching projects_ordered_by_id_desc
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -842,7 +872,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context "when sorting by #{order_by} ascendingly" do
it 'returns a properly sorted list of projects' do
- get api('/projects', current_user), params: { order_by: order_by, sort: :asc }
+ get api(path, current_user, admin_mode: true), params: { order_by: order_by, sort: :asc }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -853,7 +883,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context "when sorting by #{order_by} descendingly" do
it 'returns a properly sorted list of projects' do
- get api('/projects', current_user), params: { order_by: order_by, sort: :desc }
+ get api(path, current_user, admin_mode: true), params: { order_by: order_by, sort: :desc }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -867,7 +897,7 @@ RSpec.describe API::Projects, feature_category: :projects do
let(:current_user) { user }
it 'returns projects ordered normally' do
- get api('/projects', current_user), params: { order_by: order_by }
+ get api(path, current_user), params: { order_by: order_by }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -879,7 +909,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
end
- context 'by similarity', :aggregate_failures do
+ context 'by similarity' do
let_it_be(:group_with_projects) { create(:group) }
let_it_be(:project_1) { create(:project, name: 'Project', path: 'project', group: group_with_projects) }
let_it_be(:project_2) { create(:project, name: 'Test Project', path: 'test-project', group: group_with_projects) }
@@ -889,14 +919,14 @@ RSpec.describe API::Projects, feature_category: :projects do
let(:current_user) { user }
let(:params) { { order_by: 'similarity', search: 'test' } }
- subject { get api('/projects', current_user), params: params }
+ subject(:request) { get api(path, current_user), params: params }
before do
group_with_projects.add_owner(current_user) if current_user
end
it 'returns non-public items based ordered by similarity' do
- subject
+ request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -910,14 +940,14 @@ RSpec.describe API::Projects, feature_category: :projects do
let(:params) { { order_by: 'similarity' } }
it 'returns items ordered by created_at descending' do
- subject
+ request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response.length).to eq(8)
project_names = json_response.map { |proj| proj['name'] }
- expect(project_names).to contain_exactly(project.name, project2.name, 'second_project', 'public_project', 'Project', 'Test Project', 'Test Public Project', 'Test')
+ expect(project_names).to match_array([project, project2, project3, public_project, project_1, project_2, project_4, project_3].map(&:name))
end
end
@@ -925,14 +955,14 @@ RSpec.describe API::Projects, feature_category: :projects do
let(:current_user) { nil }
it 'returns items ordered by created_at descending' do
- subject
+ request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response.length).to eq(1)
project_names = json_response.map { |proj| proj['name'] }
- expect(project_names).to contain_exactly('Test Public Project')
+ expect(project_names).to contain_exactly(project_4.name)
end
end
end
@@ -952,6 +982,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it_behaves_like 'projects response' do
let(:filter) { { repository_storage: 'nfs-11' } }
let(:current_user) { admin }
+ let(:admin_mode) { true }
let(:projects) { [project, project3] }
end
end
@@ -974,7 +1005,7 @@ RSpec.describe API::Projects, feature_category: :projects do
let(:params) { { pagination: 'keyset', order_by: :id, sort: :asc, per_page: 1 } }
it 'includes a pagination header with link to the next page' do
- get api('/projects', current_user), params: params
+ get api(path, current_user), params: params
expect(response.header).to include('Link')
expect(response.header['Link']).to include('pagination=keyset')
@@ -982,7 +1013,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'contains only the first project with per_page = 1' do
- get api('/projects', current_user), params: params
+ get api(path, current_user), params: params
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
@@ -990,7 +1021,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'still includes a link if the end has reached and there is no more data after this page' do
- get api('/projects', current_user), params: params.merge(id_after: project2.id)
+ get api(path, current_user), params: params.merge(id_after: project2.id)
expect(response.header).to include('Link')
expect(response.header['Link']).to include('pagination=keyset')
@@ -998,20 +1029,20 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'does not include a next link when the page does not have any records' do
- get api('/projects', current_user), params: params.merge(id_after: Project.maximum(:id))
+ get api(path, current_user), params: params.merge(id_after: Project.maximum(:id))
expect(response.header).not_to include('Link')
end
it 'returns an empty array when the page does not have any records' do
- get api('/projects', current_user), params: params.merge(id_after: Project.maximum(:id))
+ get api(path, current_user), params: params.merge(id_after: Project.maximum(:id))
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq([])
end
it 'responds with 501 if order_by is different from id' do
- get api('/projects', current_user), params: params.merge(order_by: :created_at)
+ get api(path, current_user), params: params.merge(order_by: :created_at)
expect(response).to have_gitlab_http_status(:method_not_allowed)
end
@@ -1021,7 +1052,7 @@ RSpec.describe API::Projects, feature_category: :projects do
let(:params) { { pagination: 'keyset', order_by: :id, sort: :desc, per_page: 1 } }
it 'includes a pagination header with link to the next page' do
- get api('/projects', current_user), params: params
+ get api(path, current_user), params: params
expect(response.header).to include('Link')
expect(response.header['Link']).to include('pagination=keyset')
@@ -1029,7 +1060,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'contains only the last project with per_page = 1' do
- get api('/projects', current_user), params: params
+ get api(path, current_user), params: params
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
@@ -1041,7 +1072,7 @@ RSpec.describe API::Projects, feature_category: :projects do
let(:params) { { pagination: 'keyset', order_by: :id, sort: :desc, per_page: 2 } }
it 'returns all projects' do
- url = '/projects'
+ url = path
requests = 0
ids = []
@@ -1067,8 +1098,11 @@ RSpec.describe API::Projects, feature_category: :projects do
let_it_be(:admin) { create(:admin) }
+ subject(:request) { get api(path, admin) }
+
it 'avoids N+1 queries', :use_sql_query_cache do
- get api('/projects', admin)
+ request
+ expect(response).to have_gitlab_http_status(:ok)
base_project = create(:project, :public, namespace: admin.namespace)
@@ -1076,53 +1110,94 @@ RSpec.describe API::Projects, feature_category: :projects do
fork_project2 = fork_project(fork_project1, admin, namespace: create(:user).namespace)
control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api('/projects', admin)
+ request
end
fork_project(fork_project2, admin, namespace: create(:user).namespace)
expect do
- get api('/projects', admin)
- end.not_to exceed_query_limit(control.count)
+ request
+ end.not_to exceed_all_query_limit(control.count)
end
end
context 'when service desk is enabled', :use_clean_rails_memory_store_caching do
let_it_be(:admin) { create(:admin) }
+ subject(:request) { get api(path, admin) }
+
it 'avoids N+1 queries' do
- allow(Gitlab::ServiceDeskEmail).to receive(:enabled?).and_return(true)
- allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(true)
+ allow(Gitlab::Email::ServiceDeskEmail).to receive(:enabled?).and_return(true)
+ allow(Gitlab::Email::IncomingEmail).to receive(:enabled?).and_return(true)
- get api('/projects', admin)
+ request
+ expect(response).to have_gitlab_http_status(:ok)
create(:project, :public, :service_desk_enabled, namespace: admin.namespace)
control = ActiveRecord::QueryRecorder.new do
- get api('/projects', admin)
+ request
end
create_list(:project, 2, :public, :service_desk_enabled, namespace: admin.namespace)
expect do
- get api('/projects', admin)
- end.not_to exceed_query_limit(control)
+ request
+ end.not_to exceed_all_query_limit(control)
+ end
+ end
+
+ context 'rate limiting' do
+ let_it_be(:current_user) { create(:user) }
+
+ shared_examples_for 'does not log request and does not block the request' do
+ specify do
+ request
+ request
+
+ expect(response).not_to have_gitlab_http_status(:too_many_requests)
+ expect(Gitlab::AuthLogger).not_to receive(:error)
+ end
+ end
+
+ before do
+ stub_application_setting(projects_api_rate_limit_unauthenticated: 1)
+ end
+
+ context 'when the user is signed in' do
+ it_behaves_like 'does not log request and does not block the request' do
+ def request
+ get api(path, current_user)
+ end
+ end
+ end
+
+ context 'when the user is not signed in' do
+ let_it_be(:current_user) { nil }
+
+ it_behaves_like 'rate limited endpoint', rate_limit_key: :projects_api_rate_limit_unauthenticated do
+ def request
+ get api(path, current_user)
+ end
+ end
end
end
end
describe 'POST /projects' do
+ let(:path) { '/projects' }
+
context 'maximum number of projects reached' do
it 'does not create new project and respond with 403' do
allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0)
- expect { post api('/projects', user2), params: { name: 'foo' } }
+ expect { post api(path, user2), params: { name: 'foo' } }
.to change { Project.count }.by(0)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
it 'creates new project without path but with name and returns 201' do
- expect { post api('/projects', user), params: { name: 'Foo Project' } }
+ expect { post api(path, user), params: { name: 'Foo Project' } }
.to change { Project.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -1133,7 +1208,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'creates new project without name but with path and returns 201' do
- expect { post api('/projects', user), params: { path: 'foo_project' } }
+ expect { post api(path, user), params: { path: 'foo_project' } }
.to change { Project.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -1144,7 +1219,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'creates new project with name and path and returns 201' do
- expect { post api('/projects', user), params: { path: 'path-project-Foo', name: 'Foo Project' } }
+ expect { post api(path, user), params: { path: 'path-project-Foo', name: 'Foo Project' } }
.to change { Project.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -1155,21 +1230,21 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it_behaves_like 'create project with default branch parameter' do
- let(:request) { post api('/projects', user), params: params }
+ subject(:request) { post api(path, user), params: params }
end
it 'creates last project before reaching project limit' do
allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1)
- post api('/projects', user2), params: { name: 'foo' }
+ post api(path, user2), params: { name: 'foo' }
expect(response).to have_gitlab_http_status(:created)
end
it 'does not create new project without name or path and returns 400' do
- expect { post api('/projects', user) }.not_to change { Project.count }
+ expect { post api(path, user) }.not_to change { Project.count }
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'assigns attributes to project', :aggregate_failures do
+ it 'assigns attributes to project' do
project = attributes_for(:project, {
path: 'camelCasePath',
issues_enabled: false,
@@ -1189,7 +1264,6 @@ RSpec.describe API::Projects, feature_category: :projects do
merge_method: 'ff',
squash_option: 'always'
}).tap do |attrs|
- attrs[:operations_access_level] = 'disabled'
attrs[:analytics_access_level] = 'disabled'
attrs[:container_registry_access_level] = 'private'
attrs[:security_and_compliance_access_level] = 'private'
@@ -1205,7 +1279,7 @@ RSpec.describe API::Projects, feature_category: :projects do
attrs[:issues_access_level] = 'disabled'
end
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(response).to have_gitlab_http_status(:created)
@@ -1224,7 +1298,6 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED)
expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED)
- expect(project.operations_access_level).to eq(ProjectFeature::DISABLED)
expect(project.project_feature.analytics_access_level).to eq(ProjectFeature::DISABLED)
expect(project.project_feature.container_registry_access_level).to eq(ProjectFeature::PRIVATE)
expect(project.project_feature.security_and_compliance_access_level).to eq(ProjectFeature::PRIVATE)
@@ -1240,10 +1313,10 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(project.project_feature.snippets_access_level).to eq(ProjectFeature::DISABLED)
end
- it 'assigns container_registry_enabled to project', :aggregate_failures do
+ it 'assigns container_registry_enabled to project' do
project = attributes_for(:project, { container_registry_enabled: true })
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(response).to have_gitlab_http_status(:created)
expect(json_response['container_registry_enabled']).to eq(true)
@@ -1254,7 +1327,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'assigns container_registry_enabled to project' do
project = attributes_for(:project, { container_registry_enabled: true })
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(response).to have_gitlab_http_status(:created)
expect(json_response['container_registry_enabled']).to eq(true)
@@ -1262,7 +1335,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'creates a project using a template' do
- expect { post api('/projects', user), params: { template_name: 'rails', name: 'rails-test' } }
+ expect { post api(path, user), params: { template_name: 'rails', name: 'rails-test' } }
.to change { Project.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -1273,7 +1346,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'returns 400 for an invalid template' do
- expect { post api('/projects', user), params: { template_name: 'unknown', name: 'rails-test' } }
+ expect { post api(path, user), params: { template_name: 'unknown', name: 'rails-test' } }
.not_to change { Project.count }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -1282,7 +1355,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'disallows creating a project with an import_url and template' do
project_params = { import_url: 'http://example.com', template_name: 'rails', name: 'rails-test' }
- expect { post api('/projects', user), params: project_params }
+ expect { post api(path, user), params: project_params }
.not_to change { Project.count }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -1299,34 +1372,34 @@ RSpec.describe API::Projects, feature_category: :projects do
headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } })
project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' }
- expect { post api('/projects', user), params: project_params }
+ expect { post api(path, user), params: project_params }
.not_to change { Project.count }
expect(response).to have_gitlab_http_status(:forbidden)
end
- it 'allows creating a project without an import_url when git import source is disabled', :aggregate_failures do
+ it 'allows creating a project without an import_url when git import source is disabled' do
stub_application_setting(import_sources: nil)
project_params = { path: 'path-project-Foo' }
- expect { post api('/projects', user), params: project_params }.to change { Project.count }.by(1)
+ expect { post api(path, user), params: project_params }.to change { Project.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
end
- it 'disallows creating a project with an import_url that is not reachable', :aggregate_failures do
+ it 'disallows creating a project with an import_url that is not reachable' do
url = 'http://example.com'
endpoint_url = "#{url}/info/refs?service=git-upload-pack"
stub_full_request(endpoint_url, method: :get).to_return({ status: 301, body: '', headers: nil })
project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' }
- expect { post api('/projects', user), params: project_params }.not_to change { Project.count }
+ expect { post api(path, user), params: project_params }.not_to change { Project.count }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['message']).to eq("#{url} is not a valid HTTP Git repository")
end
- it 'creates a project with an import_url that is valid', :aggregate_failures do
+ it 'creates a project with an import_url that is valid' do
url = 'http://example.com'
endpoint_url = "#{url}/info/refs?service=git-upload-pack"
git_response = {
@@ -1334,10 +1407,11 @@ RSpec.describe API::Projects, feature_category: :projects do
body: '001e# service=git-upload-pack',
headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' }
}
+ stub_application_setting(import_sources: ['git'])
stub_full_request(endpoint_url, method: :get).to_return(git_response)
project_params = { import_url: url, path: 'path-project-Foo', name: 'Foo Project' }
- expect { post api('/projects', user), params: project_params }.to change { Project.count }.by(1)
+ expect { post api(path, user), params: project_params }.to change { Project.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
end
@@ -1345,7 +1419,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as public' do
project = attributes_for(:project, visibility: 'public')
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(json_response['visibility']).to eq('public')
end
@@ -1353,7 +1427,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as internal' do
project = attributes_for(:project, visibility: 'internal')
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(json_response['visibility']).to eq('internal')
end
@@ -1361,23 +1435,23 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as private' do
project = attributes_for(:project, visibility: 'private')
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(json_response['visibility']).to eq('private')
end
it 'creates a new project initialized with a README.md' do
- project = attributes_for(:project, initialize_with_readme: 1, name: 'somewhere')
+ project = attributes_for(:project, initialize_with_readme: 1)
- post api('/projects', user), params: project
+ post api(path, user), params: project
- expect(json_response['readme_url']).to eql("#{Gitlab.config.gitlab.url}/#{json_response['namespace']['full_path']}/somewhere/-/blob/master/README.md")
+ expect(json_response['readme_url']).to eql("#{Gitlab.config.gitlab.url}/#{json_response['namespace']['full_path']}/#{json_response['path']}/-/blob/master/README.md")
end
it 'sets tag list to a project (deprecated)' do
project = attributes_for(:project, tag_list: %w[tagFirst tagSecond])
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(json_response['topics']).to eq(%w[tagFirst tagSecond])
end
@@ -1385,7 +1459,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets topics to a project' do
project = attributes_for(:project, topics: %w[topic1 topics2])
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(json_response['topics']).to eq(%w[topic1 topics2])
end
@@ -1394,7 +1468,7 @@ RSpec.describe API::Projects, feature_category: :projects do
project = attributes_for(:project, avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif'))
workhorse_form_with_file(
- api('/projects', user),
+ api(path, user),
method: :post,
file_key: :avatar,
params: project
@@ -1407,7 +1481,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as not allowing outdated diff discussions to automatically resolve' do
project = attributes_for(:project, resolve_outdated_diff_discussions: false)
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(json_response['resolve_outdated_diff_discussions']).to be_falsey
end
@@ -1415,7 +1489,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as allowing outdated diff discussions to automatically resolve' do
project = attributes_for(:project, resolve_outdated_diff_discussions: true)
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(json_response['resolve_outdated_diff_discussions']).to be_truthy
end
@@ -1423,7 +1497,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as not removing source branches' do
project = attributes_for(:project, remove_source_branch_after_merge: false)
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(json_response['remove_source_branch_after_merge']).to be_falsey
end
@@ -1431,7 +1505,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as removing source branches' do
project = attributes_for(:project, remove_source_branch_after_merge: true)
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(json_response['remove_source_branch_after_merge']).to be_truthy
end
@@ -1439,7 +1513,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as allowing merge even if build fails' do
project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: false)
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey
end
@@ -1447,7 +1521,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do
project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: true)
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy
end
@@ -1455,7 +1529,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as not allowing merge when pipeline is skipped' do
project_params = attributes_for(:project, allow_merge_on_skipped_pipeline: false)
- post api('/projects', user), params: project_params
+ post api(path, user), params: project_params
expect(json_response['allow_merge_on_skipped_pipeline']).to be_falsey
end
@@ -1463,7 +1537,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as allowing merge when pipeline is skipped' do
project_params = attributes_for(:project, allow_merge_on_skipped_pipeline: true)
- post api('/projects', user), params: project_params
+ post api(path, user), params: project_params
expect(json_response['allow_merge_on_skipped_pipeline']).to be_truthy
end
@@ -1471,7 +1545,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as allowing merge even if discussions are unresolved' do
project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: false)
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
end
@@ -1479,7 +1553,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as allowing merge if only_allow_merge_if_all_discussions_are_resolved is nil' do
project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: nil)
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
end
@@ -1487,7 +1561,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as allowing merge only if all discussions are resolved' do
project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: true)
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
end
@@ -1495,7 +1569,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as enabling auto close referenced issues' do
project = attributes_for(:project, autoclose_referenced_issues: true)
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(json_response['autoclose_referenced_issues']).to be_truthy
end
@@ -1503,7 +1577,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as disabling auto close referenced issues' do
project = attributes_for(:project, autoclose_referenced_issues: false)
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(json_response['autoclose_referenced_issues']).to be_falsey
end
@@ -1511,7 +1585,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets the merge method of a project to rebase merge' do
project = attributes_for(:project, merge_method: 'rebase_merge')
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(json_response['merge_method']).to eq('rebase_merge')
end
@@ -1519,7 +1593,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'rejects invalid values for merge_method' do
project = attributes_for(:project, merge_method: 'totally_not_valid_method')
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -1527,7 +1601,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'ignores import_url when it is nil' do
project = attributes_for(:project, import_url: nil)
- post api('/projects', user), params: project
+ post api(path, user), params: project
expect(response).to have_gitlab_http_status(:created)
end
@@ -1540,7 +1614,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'does not allow a non-admin to use a restricted visibility level' do
- post api('/projects', user), params: project_param
+ post api(path, user), params: project_param
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['visibility_level'].first).to(
@@ -1549,7 +1623,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'allows an admin to override restricted visibility settings' do
- post api('/projects', admin), params: project_param
+ post api(path, admin), params: project_param
expect(json_response['visibility']).to eq('public')
end
@@ -1557,7 +1631,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'GET /users/:user_id/projects/' do
- let!(:public_project) { create(:project, :public, name: 'public_project', creator_id: user4.id, namespace: user4.namespace) }
+ let_it_be(:public_project) { create(:project, :public, creator_id: user4.id, namespace: user4.namespace) }
it 'returns error when user not found' do
get api("/users/#{non_existing_record_id}/projects/")
@@ -1575,7 +1649,7 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id)
end
- it 'includes container_registry_access_level', :aggregate_failures do
+ it 'includes container_registry_access_level' do
get api("/users/#{user4.id}/projects/", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -1583,8 +1657,18 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(json_response.first.keys).to include('container_registry_access_level')
end
+ context 'filter by updated_at' do
+ it 'returns only projects updated on the given timeframe' do
+ get api("/users/#{user.id}/projects", user),
+ params: { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.map { |project| project['id'] }).to contain_exactly(project2.id, project.id)
+ end
+ end
+
context 'and using id_after' do
- let!(:another_public_project) { create(:project, :public, name: 'another_public_project', creator_id: user4.id, namespace: user4.namespace) }
+ let_it_be(:another_public_project) { create(:project, :public, creator_id: user4.id, namespace: user4.namespace) }
it 'only returns projects with id_after filter given' do
get api("/users/#{user4.id}/projects?id_after=#{public_project.id}", user)
@@ -1606,7 +1690,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
context 'and using id_before' do
- let!(:another_public_project) { create(:project, :public, name: 'another_public_project', creator_id: user4.id, namespace: user4.namespace) }
+ let_it_be(:another_public_project) { create(:project, :public, creator_id: user4.id, namespace: user4.namespace) }
it 'only returns projects with id_before filter given' do
get api("/users/#{user4.id}/projects?id_before=#{another_public_project.id}", user)
@@ -1628,7 +1712,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
context 'and using both id_before and id_after' do
- let!(:more_projects) { create_list(:project, 5, :public, creator_id: user4.id, namespace: user4.namespace) }
+ let_it_be(:more_projects) { create_list(:project, 5, :public, creator_id: user4.id, namespace: user4.namespace) }
it 'only returns projects with id matching the range' do
get api("/users/#{user4.id}/projects?id_after=#{more_projects.first.id}&id_before=#{more_projects.last.id}", user)
@@ -1663,7 +1747,7 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(json_response.map { |project| project['id'] }).to contain_exactly(private_project1.id)
end
- context 'and using an admin to search', :enable_admin_mode, :aggregate_errors do
+ context 'and using an admin to search', :enable_admin_mode do
it 'returns users projects when authenticated as admin' do
private_project1 = create(:project, :private, name: 'private_project1', creator_id: user4.id, namespace: user4.namespace)
@@ -1697,6 +1781,8 @@ RSpec.describe API::Projects, feature_category: :projects do
user3.reload
end
+ let(:path) { "/users/#{user3.id}/starred_projects/" }
+
it 'returns error when user not found' do
get api("/users/#{non_existing_record_id}/starred_projects/")
@@ -1706,7 +1792,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'with a public profile' do
it 'returns projects filtered by user' do
- get api("/users/#{user3.id}/starred_projects/", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -1714,6 +1800,16 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(json_response.map { |project| project['id'] })
.to contain_exactly(project.id, project2.id, project3.id)
end
+
+ context 'filter by updated_at' do
+ it 'returns only projects updated on the given timeframe' do
+ get api(path, user),
+ params: { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.map { |project| project['id'] }).to contain_exactly(project2.id, project.id)
+ end
+ end
end
context 'with a private profile' do
@@ -1724,7 +1820,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'user does not have access to view the private profile' do
it 'returns no projects' do
- get api("/users/#{user3.id}/starred_projects/", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -1735,7 +1831,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'user has access to view the private profile' do
it 'returns projects filtered by user' do
- get api("/users/#{user3.id}/starred_projects/", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -1748,8 +1844,14 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'POST /projects/user/:id' do
+ let(:path) { "/projects/user/#{user.id}" }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { { name: 'Foo Project' } }
+ end
+
it 'creates new project without path but with name and return 201' do
- expect { post api("/projects/user/#{user.id}", admin), params: { name: 'Foo Project' } }.to change { Project.count }.by(1)
+ expect { post api(path, admin, admin_mode: true), params: { name: 'Foo Project' } }.to change { Project.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
project = Project.find(json_response['id'])
@@ -1759,7 +1861,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'creates new project with name and path and returns 201' do
- expect { post api("/projects/user/#{user.id}", admin), params: { path: 'path-project-Foo', name: 'Foo Project' } }
+ expect { post api(path, admin, admin_mode: true), params: { path: 'path-project-Foo', name: 'Foo Project' } }
.to change { Project.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -1770,11 +1872,11 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it_behaves_like 'create project with default branch parameter' do
- let(:request) { post api("/projects/user/#{user.id}", admin), params: params }
+ subject(:request) { post api(path, admin, admin_mode: true), params: params }
end
it 'responds with 400 on failure and not project' do
- expect { post api("/projects/user/#{user.id}", admin) }
+ expect { post api(path, admin, admin_mode: true) }
.not_to change { Project.count }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -1786,7 +1888,7 @@ RSpec.describe API::Projects, feature_category: :projects do
attrs[:container_registry_enabled] = true
end
- post api("/projects/user/#{user.id}", admin), params: project
+ post api(path, admin, admin_mode: true), params: project
expect(response).to have_gitlab_http_status(:created)
expect(json_response['container_registry_enabled']).to eq(true)
@@ -1802,7 +1904,7 @@ RSpec.describe API::Projects, feature_category: :projects do
jobs_enabled: true
})
- post api("/projects/user/#{user.id}", admin), params: project
+ post api(path, admin, admin_mode: true), params: project
expect(response).to have_gitlab_http_status(:created)
@@ -1816,7 +1918,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as public' do
project = attributes_for(:project, visibility: 'public')
- post api("/projects/user/#{user.id}", admin), params: project
+ post api(path, admin, admin_mode: true), params: project
expect(response).to have_gitlab_http_status(:created)
expect(json_response['visibility']).to eq('public')
@@ -1825,7 +1927,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as internal' do
project = attributes_for(:project, visibility: 'internal')
- post api("/projects/user/#{user.id}", admin), params: project
+ post api(path, admin, admin_mode: true), params: project
expect(response).to have_gitlab_http_status(:created)
expect(json_response['visibility']).to eq('internal')
@@ -1834,7 +1936,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as private' do
project = attributes_for(:project, visibility: 'private')
- post api("/projects/user/#{user.id}", admin), params: project
+ post api(path, admin, admin_mode: true), params: project
expect(json_response['visibility']).to eq('private')
end
@@ -1842,7 +1944,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as not allowing outdated diff discussions to automatically resolve' do
project = attributes_for(:project, resolve_outdated_diff_discussions: false)
- post api("/projects/user/#{user.id}", admin), params: project
+ post api(path, admin, admin_mode: true), params: project
expect(json_response['resolve_outdated_diff_discussions']).to be_falsey
end
@@ -1850,7 +1952,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as allowing outdated diff discussions to automatically resolve' do
project = attributes_for(:project, resolve_outdated_diff_discussions: true)
- post api("/projects/user/#{user.id}", admin), params: project
+ post api(path, admin, admin_mode: true), params: project
expect(json_response['resolve_outdated_diff_discussions']).to be_truthy
end
@@ -1858,7 +1960,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as not removing source branches' do
project = attributes_for(:project, remove_source_branch_after_merge: false)
- post api("/projects/user/#{user.id}", admin), params: project
+ post api(path, admin, admin_mode: true), params: project
expect(json_response['remove_source_branch_after_merge']).to be_falsey
end
@@ -1866,7 +1968,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as removing source branches' do
project = attributes_for(:project, remove_source_branch_after_merge: true)
- post api("/projects/user/#{user.id}", admin), params: project
+ post api(path, admin, admin_mode: true), params: project
expect(json_response['remove_source_branch_after_merge']).to be_truthy
end
@@ -1874,7 +1976,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as allowing merge even if build fails' do
project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: false)
- post api("/projects/user/#{user.id}", admin), params: project
+ post api(path, admin, admin_mode: true), params: project
expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey
end
@@ -1882,7 +1984,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as allowing merge only if pipeline succeeds' do
project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: true)
- post api("/projects/user/#{user.id}", admin), params: project
+ post api(path, admin, admin_mode: true), params: project
expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy
end
@@ -1890,7 +1992,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as not allowing merge when pipeline is skipped' do
project = attributes_for(:project, allow_merge_on_skipped_pipeline: false)
- post api("/projects/user/#{user.id}", admin), params: project
+ post api(path, admin, admin_mode: true), params: project
expect(json_response['allow_merge_on_skipped_pipeline']).to be_falsey
end
@@ -1898,7 +2000,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as allowing merge when pipeline is skipped' do
project = attributes_for(:project, allow_merge_on_skipped_pipeline: true)
- post api("/projects/user/#{user.id}", admin), params: project
+ post api(path, admin, admin_mode: true), params: project
expect(json_response['allow_merge_on_skipped_pipeline']).to be_truthy
end
@@ -1906,7 +2008,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as allowing merge even if discussions are unresolved' do
project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: false)
- post api("/projects/user/#{user.id}", admin), params: project
+ post api(path, admin, admin_mode: true), params: project
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
end
@@ -1914,7 +2016,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets a project as allowing merge only if all discussions are resolved' do
project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: true)
- post api("/projects/user/#{user.id}", admin), params: project
+ post api(path, admin, admin_mode: true), params: project
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
end
@@ -1928,12 +2030,12 @@ RSpec.describe API::Projects, feature_category: :projects do
end
with_them do
- it 'setting container_registry_enabled also sets container_registry_access_level', :aggregate_failures do
+ it 'setting container_registry_enabled also sets container_registry_access_level' do
project_attributes = attributes_for(:project).tap do |attrs|
attrs[:container_registry_enabled] = container_registry_enabled
end
- post api("/projects/user/#{user.id}", admin), params: project_attributes
+ post api(path, admin, admin_mode: true), params: project_attributes
project = Project.find_by(path: project_attributes[:path])
expect(response).to have_gitlab_http_status(:created)
@@ -1955,12 +2057,12 @@ RSpec.describe API::Projects, feature_category: :projects do
end
with_them do
- it 'setting container_registry_access_level also sets container_registry_enabled', :aggregate_failures do
+ it 'setting container_registry_access_level also sets container_registry_enabled' do
project_attributes = attributes_for(:project).tap do |attrs|
attrs[:container_registry_access_level] = container_registry_access_level
end
- post api("/projects/user/#{user.id}", admin), params: project_attributes
+ post api(path, admin, admin_mode: true), params: project_attributes
project = Project.find_by(path: project_attributes[:path])
expect(response).to have_gitlab_http_status(:created)
@@ -1975,10 +2077,11 @@ RSpec.describe API::Projects, feature_category: :projects do
describe "POST /projects/:id/uploads/authorize" do
let(:headers) { workhorse_internal_api_request_header.merge({ 'HTTP_GITLAB_WORKHORSE' => 1 }) }
+ let(:path) { "/projects/#{project.id}/uploads/authorize" }
context 'with authorized user' do
it "returns 200" do
- post api("/projects/#{project.id}/uploads/authorize", user), headers: headers
+ post api(path, user), headers: headers
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['MaximumSize']).to eq(project.max_attachment_size)
@@ -1987,7 +2090,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'with unauthorized user' do
it "returns 404" do
- post api("/projects/#{project.id}/uploads/authorize", user2), headers: headers
+ post api(path, user2), headers: headers
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -1999,20 +2102,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it "returns 200" do
- post api("/projects/#{project.id}/uploads/authorize", user), headers: headers
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['MaximumSize']).to eq(1.gigabyte)
- end
- end
-
- context 'with upload size enforcement disabled' do
- before do
- stub_feature_flags(enforce_max_attachment_size_upload_api: false)
- end
-
- it "returns 200" do
- post api("/projects/#{project.id}/uploads/authorize", user), headers: headers
+ post api(path, user), headers: headers
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['MaximumSize']).to eq(1.gigabyte)
@@ -2021,7 +2111,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'with no Workhorse headers' do
it "returns 403" do
- post api("/projects/#{project.id}/uploads/authorize", user)
+ post api(path, user)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -2030,6 +2120,7 @@ RSpec.describe API::Projects, feature_category: :projects do
describe "POST /projects/:id/uploads" do
let(:file) { fixture_file_upload("spec/fixtures/dk.png", "image/png") }
+ let(:path) { "/projects/#{project.id}/uploads" }
before do
project
@@ -2040,7 +2131,7 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(instance).to receive(:override_max_attachment_size=).with(project.max_attachment_size).and_call_original
end
- post api("/projects/#{project.id}/uploads", user), params: { file: file }
+ post api(path, user), params: { file: file }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['alt']).to eq("dk")
@@ -2060,7 +2151,7 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(path).not_to be(nil)
expect(Rack::Multipart::Parser::TEMPFILE_FACTORY).to receive(:call).and_return(tempfile)
- post api("/projects/#{project.id}/uploads", user), params: { file: fixture_file_upload("spec/fixtures/dk.png", "image/png") }
+ post api(path, user), params: { file: fixture_file_upload("spec/fixtures/dk.png", "image/png") }
expect(tempfile.path).to be(nil)
expect(File.exist?(path)).to be(false)
@@ -2072,7 +2163,7 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(instance).to receive(:override_max_attachment_size=).with(1.gigabyte).and_call_original
end
- post api("/projects/#{project.id}/uploads", user), params: { file: file }
+ post api(path, user), params: { file: file }
expect(response).to have_gitlab_http_status(:created)
end
@@ -2084,7 +2175,7 @@ RSpec.describe API::Projects, feature_category: :projects do
hash_including(message: 'File exceeds maximum size', upload_allowed: upload_allowed))
.and_call_original
- post api("/projects/#{project.id}/uploads", user), params: { file: file }
+ post api(path, user), params: { file: file }
end
end
@@ -2095,14 +2186,6 @@ RSpec.describe API::Projects, feature_category: :projects do
it_behaves_like 'capped upload attachments', true
end
-
- context 'with upload size enforcement disabled' do
- before do
- stub_feature_flags(enforce_max_attachment_size_upload_api: false)
- end
-
- it_behaves_like 'capped upload attachments', false
- end
end
describe "GET /projects/:id/groups" do
@@ -2113,33 +2196,37 @@ RSpec.describe API::Projects, feature_category: :projects do
let_it_be(:private_project) { create(:project, :private, group: project_group) }
let_it_be(:public_project) { create(:project, :public, group: project_group) }
+ let(:path) { "/projects/#{private_project.id}/groups" }
+
before_all do
create(:project_group_link, :developer, group: shared_group_with_dev_access, project: private_project)
create(:project_group_link, :reporter, group: shared_group_with_reporter_access, project: private_project)
end
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:failed_status_code) { :not_found }
+ end
+
shared_examples_for 'successful groups response' do
it 'returns an array of groups' do
request
- aggregate_failures do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.map { |g| g['name'] }).to match_array(expected_groups.map(&:name))
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.map { |g| g['name'] }).to match_array(expected_groups.map(&:name))
end
end
context 'when unauthenticated' do
it 'does not return groups for private projects' do
- get api("/projects/#{private_project.id}/groups")
+ get api(path)
expect(response).to have_gitlab_http_status(:not_found)
end
context 'for public projects' do
- let(:request) { get api("/projects/#{public_project.id}/groups") }
+ subject(:request) { get api("/projects/#{public_project.id}/groups") }
it_behaves_like 'successful groups response' do
let(:expected_groups) { [root_group, project_group] }
@@ -2150,14 +2237,15 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'when authenticated as user' do
context 'when user does not have access to the project' do
it 'does not return groups' do
- get api("/projects/#{private_project.id}/groups", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when user has access to the project' do
- let(:request) { get api("/projects/#{private_project.id}/groups", user), params: params }
+ subject(:request) { get api(path, user), params: params }
+
let(:params) { {} }
before do
@@ -2219,7 +2307,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
context 'when authenticated as admin' do
- let(:request) { get api("/projects/#{private_project.id}/groups", admin) }
+ subject(:request) { get api(path, admin, admin_mode: true) }
it_behaves_like 'successful groups response' do
let(:expected_groups) { [root_group, project_group] }
@@ -2228,27 +2316,30 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'GET /project/:id/share_locations' do
- let_it_be(:root_group) { create(:group, :public, name: 'root group') }
- let_it_be(:project_group1) { create(:group, :public, parent: root_group, name: 'group1') }
- let_it_be(:project_group2) { create(:group, :public, parent: root_group, name: 'group2') }
+ let_it_be(:root_group) { create(:group, :public, name: 'root group', path: 'root-group-path') }
+ let_it_be(:project_group1) { create(:group, :public, parent: root_group, name: 'group1', path: 'group-1-path') }
+ let_it_be(:project_group2) { create(:group, :public, parent: root_group, name: 'group2', path: 'group-2-path') }
let_it_be(:project) { create(:project, :private, group: project_group1) }
+ let(:path) { "/projects/#{project.id}/share_locations" }
+
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:failed_status_code) { :not_found }
+ end
shared_examples_for 'successful groups response' do
it 'returns an array of groups' do
request
- aggregate_failures do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.map { |g| g['name'] }).to match_array(expected_groups.map(&:name))
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.map { |g| g['name'] }).to match_array(expected_groups.map(&:name))
end
end
context 'when unauthenticated' do
it 'does not return the groups for the given project' do
- get api("/projects/#{project.id}/share_locations")
+ get api(path)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -2257,14 +2348,15 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'when authenticated' do
context 'when user is not the owner of the project' do
it 'does not return the groups' do
- get api("/projects/#{project.id}/share_locations", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when user is the owner of the project' do
- let(:request) { get api("/projects/#{project.id}/share_locations", user), params: params }
+ subject(:request) { get api(path, user), params: params }
+
let(:params) { {} }
before do
@@ -2275,26 +2367,38 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'with default search' do
it_behaves_like 'successful groups response' do
- let(:expected_groups) { [project_group1, project_group2] }
+ let(:expected_groups) { [project_group2] }
end
end
context 'when searching by group name' do
- let(:params) { { search: 'group1' } }
+ context 'searching by group name' do
+ it_behaves_like 'successful groups response' do
+ let(:params) { { search: 'group2' } }
+ let(:expected_groups) { [project_group2] }
+ end
+ end
- it_behaves_like 'successful groups response' do
- let(:expected_groups) { [project_group1] }
+ context 'searching by full group path' do
+ let_it_be(:project_group2_subgroup) do
+ create(:group, :public, parent: project_group2, name: 'subgroup', path: 'subgroup-path')
+ end
+
+ it_behaves_like 'successful groups response' do
+ let(:params) { { search: 'root-group-path/group-2-path/subgroup-path' } }
+ let(:expected_groups) { [project_group2_subgroup] }
+ end
end
end
end
end
context 'when authenticated as admin' do
- let(:request) { get api("/projects/#{project.id}/share_locations", admin), params: {} }
+ subject(:request) { get api(path, admin, admin_mode: true), params: {} }
context 'without share_with_group_lock' do
it_behaves_like 'successful groups response' do
- let(:expected_groups) { [root_group, project_group1, project_group2] }
+ let(:expected_groups) { [project_group2] }
end
end
@@ -2311,6 +2415,12 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'GET /projects/:id' do
+ let(:path) { "/projects/#{project.id}" }
+
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:failed_status_code) { :not_found }
+ end
+
context 'when unauthenticated' do
it 'does not return private projects' do
private_project = create(:project, :private)
@@ -2350,7 +2460,7 @@ RSpec.describe API::Projects, feature_category: :projects do
let(:protected_attributes) { %w(default_branch ci_config_path) }
it 'hides protected attributes of private repositories if user is not a member' do
- get api("/projects/#{project.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
protected_attributes.each do |attribute|
@@ -2361,7 +2471,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'exposes protected attributes of private repositories if user is a member' do
project.add_developer(user)
- get api("/projects/#{project.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
protected_attributes.each do |attribute|
@@ -2408,17 +2518,18 @@ RSpec.describe API::Projects, feature_category: :projects do
keys
end
- it 'returns a project by id', :aggregate_failures do
+ it 'returns a project by id' do
project
project_member
group = create(:group)
link = create(:project_group_link, project: project, group: group)
- get api("/projects/#{project.id}", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(project.id)
expect(json_response['description']).to eq(project.description)
+ expect(json_response['description_html']).to eq(project.description_html)
expect(json_response['default_branch']).to eq(project.default_branch)
expect(json_response['tag_list']).to be_an Array # deprecated in favor of 'topics'
expect(json_response['topics']).to be_an Array
@@ -2440,6 +2551,7 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(json_response['container_registry_enabled']).to be_present
expect(json_response['container_registry_access_level']).to be_present
expect(json_response['created_at']).to be_present
+ expect(json_response['updated_at']).to be_present
expect(json_response['last_activity_at']).to be_present
expect(json_response['shared_runners_enabled']).to be_present
expect(json_response['group_runners_enabled']).to be_present
@@ -2458,7 +2570,6 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(json_response['allow_merge_on_skipped_pipeline']).to eq(project.allow_merge_on_skipped_pipeline)
expect(json_response['restrict_user_defined_variables']).to eq(project.restrict_user_defined_variables?)
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
- expect(json_response['operations_access_level']).to be_present
expect(json_response['security_and_compliance_access_level']).to be_present
expect(json_response['releases_access_level']).to be_present
expect(json_response['environments_access_level']).to be_present
@@ -2470,19 +2581,19 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'exposes all necessary attributes' do
create(:project_group_link, project: project)
- get api("/projects/#{project.id}", admin)
+ get api(path, admin, admin_mode: true)
diff = Set.new(json_response.keys) ^ Set.new(expected_keys)
expect(diff).to be_empty, failure_message(diff)
end
- def failure_message(diff)
+ def failure_message(_diff)
<<~MSG
It looks like project's set of exposed attributes is different from the expected set.
The following attributes are missing or newly added:
- #{diff.to_a.to_sentence}
+ {diff.to_a.to_sentence}
Please update #{project_attributes_file} file"
MSG
@@ -2496,11 +2607,11 @@ RSpec.describe API::Projects, feature_category: :projects do
stub_container_registry_config(enabled: true, host_port: 'registry.example.org:5000')
end
- it 'returns a project by id', :aggregate_failures do
+ it 'returns a project by id' do
group = create(:group)
link = create(:project_group_link, project: project, group: group)
- get api("/projects/#{project.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(project.id)
@@ -2532,7 +2643,6 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(json_response['analytics_access_level']).to be_present
expect(json_response['wiki_access_level']).to be_present
expect(json_response['builds_access_level']).to be_present
- expect(json_response['operations_access_level']).to be_present
expect(json_response['security_and_compliance_access_level']).to be_present
expect(json_response['releases_access_level']).to be_present
expect(json_response['environments_access_level']).to be_present
@@ -2584,7 +2694,7 @@ RSpec.describe API::Projects, feature_category: :projects do
expires_at = 5.days.from_now.to_date
link = create(:project_group_link, project: project, group: group, expires_at: expires_at)
- get api("/projects/#{project.id}", user)
+ get api(path, user)
expect(json_response['shared_with_groups']).to be_an Array
expect(json_response['shared_with_groups'].length).to eq(1)
@@ -2596,7 +2706,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'returns a project by path name' do
- get api("/projects/#{project.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(project.name)
end
@@ -2609,7 +2719,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'returns a 404 error if user is not a member' do
other_user = create(:user)
- get api("/projects/#{project.id}", other_user)
+ get api(path, other_user)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -2623,7 +2733,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'exposes namespace fields' do
- get api("/projects/#{project.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['namespace']).to eq({
@@ -2639,14 +2749,14 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it "does not include license fields by default" do
- get api("/projects/#{project.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).not_to include('license', 'license_url')
end
it 'includes license fields when requested' do
- get api("/projects/#{project.id}", user), params: { license: true }
+ get api(path, user), params: { license: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['license']).to eq({
@@ -2659,14 +2769,14 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it "does not include statistics by default" do
- get api("/projects/#{project.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).not_to include 'statistics'
end
it "includes statistics if requested" do
- get api("/projects/#{project.id}", user), params: { statistics: true }
+ get api(path, user), params: { statistics: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to include 'statistics'
@@ -2676,7 +2786,7 @@ RSpec.describe API::Projects, feature_category: :projects do
let(:project) { create(:project, :public, :repository, :repository_private) }
it "does not include statistics if user is not a member" do
- get api("/projects/#{project.id}", user), params: { statistics: true }
+ get api(path, user), params: { statistics: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).not_to include 'statistics'
@@ -2685,7 +2795,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it "includes statistics if user is a member" do
project.add_developer(user)
- get api("/projects/#{project.id}", user), params: { statistics: true }
+ get api(path, user), params: { statistics: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to include 'statistics'
@@ -2695,7 +2805,7 @@ RSpec.describe API::Projects, feature_category: :projects do
project.add_developer(user)
project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
- get api("/projects/#{project.id}", user), params: { statistics: true }
+ get api(path, user), params: { statistics: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to include 'statistics'
@@ -2703,14 +2813,14 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it "includes import_error if user can admin project" do
- get api("/projects/#{project.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to include("import_error")
end
it "does not include import_error if user cannot admin project" do
- get api("/projects/#{project.id}", user3)
+ get api(path, user3)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).not_to include("import_error")
@@ -2719,7 +2829,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'returns 404 when project is marked for deletion' do
project.update!(pending_delete: true)
- get api("/projects/#{project.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Project Not Found')
@@ -2727,7 +2837,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'links exposure' do
it 'exposes related resources full URIs' do
- get api("/projects/#{project.id}", user)
+ get api(path, user)
links = json_response['_links']
@@ -2801,7 +2911,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'personal project' do
it 'sets project access and returns 200' do
project.add_maintainer(user)
- get api("/projects/#{project.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['permissions']['project_access']['access_level'])
@@ -2868,7 +2978,7 @@ RSpec.describe API::Projects, feature_category: :projects do
let!(:project_member) { create(:project_member, :developer, user: user, project: project) }
it 'returns group web_url and avatar_url' do
- get api("/projects/#{project.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
@@ -2883,7 +2993,7 @@ RSpec.describe API::Projects, feature_category: :projects do
let(:project) { create(:project, namespace: user.namespace) }
it 'returns user web_url and avatar_url' do
- get api("/projects/#{project.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
@@ -2894,21 +3004,61 @@ RSpec.describe API::Projects, feature_category: :projects do
end
end
+ context 'when authenticated as a developer' do
+ before do
+ project
+ project_member
+ end
+
+ it 'hides sensitive admin attributes' do
+ get api(path, user3)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq(project.id)
+ expect(json_response['description']).to eq(project.description)
+ expect(json_response['default_branch']).to eq(project.default_branch)
+ expect(json_response['ci_config_path']).to eq(project.ci_config_path)
+ expect(json_response['forked_from_project']).to eq(project.forked_from_project)
+ expect(json_response['service_desk_address']).to eq(project.service_desk_address)
+ expect(json_response).not_to include(
+ 'ci_default_git_depth',
+ 'ci_forward_deployment_enabled',
+ 'ci_job_token_scope_enabled',
+ 'ci_separated_caches',
+ 'ci_allow_fork_pipelines_to_run_in_parent_project',
+ 'build_git_strategy',
+ 'keep_latest_artifact',
+ 'restrict_user_defined_variables',
+ 'runners_token',
+ 'runner_token_expiration_interval',
+ 'group_runners_enabled',
+ 'auto_cancel_pending_pipelines',
+ 'build_timeout',
+ 'auto_devops_enabled',
+ 'auto_devops_deploy_strategy',
+ 'import_error'
+ )
+ end
+ end
+
it_behaves_like 'storing arguments in the application context for the API' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let(:expected_params) { { user: user.username, project: project.full_path } }
- subject { get api("/projects/#{project.id}", user) }
+ subject { get api(path, user) }
end
describe 'repository_storage attribute' do
+ let_it_be(:admin_mode) { false }
+
before do
- get api("/projects/#{project.id}", user)
+ get api(path, user, admin_mode: admin_mode)
end
context 'when authenticated as an admin' do
let(:user) { create(:admin) }
+ let_it_be(:admin_mode) { true }
it 'returns repository_storage attribute' do
expect(response).to have_gitlab_http_status(:ok)
@@ -2924,31 +3074,34 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'exposes service desk attributes' do
- get api("/projects/#{project.id}", user)
+ get api(path, user)
expect(json_response).to have_key 'service_desk_enabled'
expect(json_response).to have_key 'service_desk_address'
end
context 'when project is shared to multiple groups' do
- it 'avoids N+1 queries' do
+ it 'avoids N+1 queries', :use_sql_query_cache do
create(:project_group_link, project: project)
- get api("/projects/#{project.id}", user)
+ get api(path, user)
+ expect(response).to have_gitlab_http_status(:ok)
control = ActiveRecord::QueryRecorder.new do
- get api("/projects/#{project.id}", user)
+ get api(path, user)
end
create(:project_group_link, project: project)
expect do
- get api("/projects/#{project.id}", user)
+ get api(path, user)
end.not_to exceed_query_limit(control)
end
end
end
describe 'GET /projects/:id/users' do
+ let(:path) { "/projects/#{project.id}/users" }
+
shared_examples_for 'project users response' do
let(:reporter_1) { create(:user) }
let(:reporter_2) { create(:user) }
@@ -2959,7 +3112,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'returns the project users' do
- get api("/projects/#{project.id}/users", current_user)
+ get api(path, current_user)
user = project.namespace.first_owner
@@ -2978,6 +3131,10 @@ RSpec.describe API::Projects, feature_category: :projects do
end
end
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:failed_status_code) { :not_found }
+ end
+
context 'when unauthenticated' do
it_behaves_like 'project users response' do
let(:project) { create(:project, :public) }
@@ -3003,7 +3160,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'returns a 404 error if user is not a member' do
other_user = create(:user)
- get api("/projects/#{project.id}/users", other_user)
+ get api(path, other_user)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -3022,18 +3179,25 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'fork management' do
- let(:project_fork_target) { create(:project) }
- let(:project_fork_source) { create(:project, :public) }
- let(:private_project_fork_source) { create(:project, :private) }
+ let_it_be_with_refind(:project_fork_target) { create(:project) }
+ let_it_be_with_refind(:project_fork_source) { create(:project, :public) }
+ let_it_be_with_refind(:private_project_fork_source) { create(:project, :private) }
describe 'POST /projects/:id/fork/:forked_from_id' do
+ let(:path) { "/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}" }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { {} }
+ let(:failed_status_code) { :not_found }
+ end
+
context 'user is a developer' do
before do
project_fork_target.add_developer(user)
end
it 'denies project to be forked from an existing project' do
- post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user)
+ post api(path, user)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -3051,7 +3215,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'allows project to be forked from an existing project' do
expect(project_fork_target).not_to be_forked
- post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user)
+ post api(path, user)
project_fork_target.reload
expect(response).to have_gitlab_http_status(:created)
@@ -3063,7 +3227,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'fails without permission from forked_from project' do
project_fork_source.project_feature.update_attribute(:forking_access_level, ProjectFeature::PRIVATE)
- post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user)
+ post api(path, user)
expect(response).to have_gitlab_http_status(:forbidden)
expect(project_fork_target.forked_from_project).to be_nil
@@ -3082,25 +3246,25 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'allows project to be forked from an existing project' do
expect(project_fork_target).not_to be_forked
- post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
+ post api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:created)
end
it 'allows project to be forked from a private project' do
- post api("/projects/#{project_fork_target.id}/fork/#{private_project_fork_source.id}", admin)
+ post api("/projects/#{project_fork_target.id}/fork/#{private_project_fork_source.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:created)
end
it 'refreshes the forks count cachce' do
expect do
- post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
+ post api(path, admin, admin_mode: true)
end.to change(project_fork_source, :forks_count).by(1)
end
it 'fails if forked_from project which does not exist' do
- post api("/projects/#{project_fork_target.id}/fork/#{non_existing_record_id}", admin)
+ post api("/projects/#{project_fork_target.id}/fork/#{non_existing_record_id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -3109,7 +3273,7 @@ RSpec.describe API::Projects, feature_category: :projects do
Projects::ForkService.new(project_fork_source, admin).execute(project_fork_target)
- post api("/projects/#{project_fork_target.id}/fork/#{other_project_fork_source.id}", admin)
+ post api("/projects/#{project_fork_target.id}/fork/#{other_project_fork_source.id}", admin, admin_mode: true)
project_fork_target.reload
expect(response).to have_gitlab_http_status(:conflict)
@@ -3120,8 +3284,10 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'DELETE /projects/:id/fork' do
+ let(:path) { "/projects/#{project_fork_target.id}/fork" }
+
it "is not visible to users outside group" do
- delete api("/projects/#{project_fork_target.id}/fork", user)
+ delete api(path, user)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -3135,14 +3301,19 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'for a forked project' do
before do
- post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
+ post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin, admin_mode: true)
project_fork_target.reload
expect(project_fork_target.forked_from_project).to be_present
expect(project_fork_target).to be_forked
end
+ it_behaves_like 'DELETE request permissions for admin mode' do
+ let(:success_status_code) { :no_content }
+ let(:failed_status_code) { :not_found }
+ end
+
it 'makes forked project unforked' do
- delete api("/projects/#{project_fork_target.id}/fork", admin)
+ delete api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
project_fork_target.reload
@@ -3151,18 +3322,18 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it_behaves_like '412 response' do
- let(:request) { api("/projects/#{project_fork_target.id}/fork", admin) }
+ subject(:request) { api(path, admin, admin_mode: true) }
end
end
it 'is forbidden to non-owner users' do
- delete api("/projects/#{project_fork_target.id}/fork", user2)
+ delete api(path, user2)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'is idempotent if not forked' do
expect(project_fork_target.forked_from_project).to be_nil
- delete api("/projects/#{project_fork_target.id}/fork", admin)
+ delete api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_modified)
expect(project_fork_target.reload.forked_from_project).to be_nil
end
@@ -3170,17 +3341,17 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'GET /projects/:id/forks' do
- let(:private_fork) { create(:project, :private, :empty_repo) }
- let(:member) { create(:user) }
- let(:non_member) { create(:user) }
+ let_it_be_with_refind(:private_fork) { create(:project, :private, :empty_repo) }
+ let_it_be(:member) { create(:user) }
+ let_it_be(:non_member) { create(:user) }
- before do
+ before_all do
private_fork.add_developer(member)
end
context 'for a forked project' do
before do
- post api("/projects/#{private_fork.id}/fork/#{project_fork_source.id}", admin)
+ post api("/projects/#{private_fork.id}/fork/#{project_fork_source.id}", admin, admin_mode: true)
private_fork.reload
expect(private_fork.forked_from_project).to be_present
expect(private_fork).to be_forked
@@ -3198,6 +3369,20 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(json_response.length).to eq(1)
expect(json_response[0]['name']).to eq(private_fork.name)
end
+
+ context 'filter by updated_at' do
+ before do
+ private_fork.update!(updated_at: 4.days.ago)
+ end
+
+ it 'returns only forks updated on the given timeframe' do
+ get api("/projects/#{project_fork_source.id}/forks", member),
+ params: { updated_before: 2.days.ago.iso8601, updated_after: 6.days.ago }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.map { |project| project['id'] }).to contain_exactly(private_fork.id)
+ end
+ end
end
context 'for a user that cannot access the forks' do
@@ -3226,6 +3411,7 @@ RSpec.describe API::Projects, feature_category: :projects do
describe "POST /projects/:id/share" do
let_it_be(:group) { create(:group, :private) }
let_it_be(:group_user) { create(:user) }
+ let(:path) { "/projects/#{project.id}/share" }
before do
group.add_developer(user)
@@ -3236,7 +3422,7 @@ RSpec.describe API::Projects, feature_category: :projects do
expires_at = 10.days.from_now.to_date
expect do
- post api("/projects/#{project.id}/share", user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at }
+ post api(path, user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at }
end.to change { ProjectGroupLink.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -3247,51 +3433,51 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'updates project authorization', :sidekiq_inline do
expect do
- post api("/projects/#{project.id}/share", user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER }
+ post api(path, user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER }
end.to(
change { group_user.can?(:read_project, project) }.from(false).to(true)
)
end
it "returns a 400 error when group id is not given" do
- post api("/projects/#{project.id}/share", user), params: { group_access: Gitlab::Access::DEVELOPER }
+ post api(path, user), params: { group_access: Gitlab::Access::DEVELOPER }
expect(response).to have_gitlab_http_status(:bad_request)
end
it "returns a 400 error when access level is not given" do
- post api("/projects/#{project.id}/share", user), params: { group_id: group.id }
+ post api(path, user), params: { group_id: group.id }
expect(response).to have_gitlab_http_status(:bad_request)
end
it "returns a 400 error when sharing is disabled" do
project.namespace.update!(share_with_group_lock: true)
- post api("/projects/#{project.id}/share", user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER }
+ post api(path, user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER }
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'returns a 404 error when user cannot read group' do
private_group = create(:group, :private)
- post api("/projects/#{project.id}/share", user), params: { group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER }
+ post api(path, user), params: { group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER }
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns a 404 error when group does not exist' do
- post api("/projects/#{project.id}/share", user), params: { group_id: non_existing_record_id, group_access: Gitlab::Access::DEVELOPER }
+ post api(path, user), params: { group_id: non_existing_record_id, group_access: Gitlab::Access::DEVELOPER }
expect(response).to have_gitlab_http_status(:not_found)
end
it "returns a 400 error when wrong params passed" do
- post api("/projects/#{project.id}/share", user), params: { group_id: group.id, group_access: non_existing_record_access_level }
+ post api(path, user), params: { group_id: group.id, group_access: non_existing_record_access_level }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq 'group_access does not have a valid value'
end
it "returns a 400 error when the project-group share is created with an OWNER access level" do
- post api("/projects/#{project.id}/share", user), params: { group_id: group.id, group_access: Gitlab::Access::OWNER }
+ post api(path, user), params: { group_id: group.id, group_access: Gitlab::Access::OWNER }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq 'group_access does not have a valid value'
@@ -3301,10 +3487,22 @@ RSpec.describe API::Projects, feature_category: :projects do
allow(::Projects::GroupLinks::CreateService).to receive_message_chain(:new, :execute)
.and_return({ status: :error, http_status: 409, message: 'error' })
- post api("/projects/#{project.id}/share", user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER }
+ post api(path, user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER }
expect(response).to have_gitlab_http_status(:conflict)
end
+
+ context 'when project is forked' do
+ let(:forked_project) { fork_project(project) }
+ let(:path) { "/projects/#{forked_project.id}/share" }
+
+ it 'returns a 404 error when group does not exist' do
+ forked_project.add_maintainer(user)
+ post api(path, user), params: { group_id: non_existing_record_id, group_access: Gitlab::Access::DEVELOPER }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
describe 'DELETE /projects/:id/share/:group_id' do
@@ -3334,7 +3532,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it_behaves_like '412 response' do
- let(:request) { api("/projects/#{project.id}/share/#{group.id}", user) }
+ subject(:request) { api("/projects/#{project.id}/share/#{group.id}", user) }
end
end
@@ -3360,6 +3558,7 @@ RSpec.describe API::Projects, feature_category: :projects do
describe 'POST /projects/:id/import_project_members/:project_id' do
let_it_be(:project2) { create(:project) }
let_it_be(:project2_user) { create(:user) }
+ let(:path) { "/projects/#{project.id}/import_project_members/#{project2.id}" }
before_all do
project.add_maintainer(user)
@@ -3368,7 +3567,8 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'records the query', :request_store, :use_sql_query_cache do
- post api("/projects/#{project.id}/import_project_members/#{project2.id}", user)
+ post api(path, user)
+ expect(response).to have_gitlab_http_status(:created)
control_project = create(:project)
control_project.add_maintainer(user)
@@ -3392,7 +3592,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'returns 200 when it successfully imports members from another project' do
expect do
- post api("/projects/#{project.id}/import_project_members/#{project2.id}", user)
+ post api(path, user)
end.to change { project.members.count }.by(2)
expect(response).to have_gitlab_http_status(:created)
@@ -3435,7 +3635,7 @@ RSpec.describe API::Projects, feature_category: :projects do
project2.add_developer(user2)
expect do
- post api("/projects/#{project.id}/import_project_members/#{project2.id}", user2)
+ post api(path, user2)
end.not_to change { project.members.count }
expect(response).to have_gitlab_http_status(:forbidden)
@@ -3448,7 +3648,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
expect do
- post api("/projects/#{project.id}/import_project_members/#{project2.id}", user)
+ post api(path, user)
end.not_to change { project.members.count }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
@@ -3457,6 +3657,8 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'PUT /projects/:id' do
+ let(:path) { "/projects/#{project.id}" }
+
before do
expect(project).to be_persisted
expect(user).to be_persisted
@@ -3468,13 +3670,18 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(project_member).to be_persisted
end
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:params) { { visibility: 'internal' } }
+ let(:failed_status_code) { :not_found }
+ end
+
describe 'updating packages_enabled attribute' do
it 'is enabled by default' do
expect(project.packages_enabled).to be true
end
it 'disables project packages feature' do
- put(api("/projects/#{project.id}", user), params: { packages_enabled: false })
+ put(api(path, user), params: { packages_enabled: false })
expect(response).to have_gitlab_http_status(:ok)
expect(project.reload.packages_enabled).to be false
@@ -3482,8 +3689,8 @@ RSpec.describe API::Projects, feature_category: :projects do
end
end
- it 'sets container_registry_access_level', :aggregate_failures do
- put api("/projects/#{project.id}", user), params: { container_registry_access_level: 'private' }
+ it 'sets container_registry_access_level' do
+ put api(path, user), params: { container_registry_access_level: 'private' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['container_registry_access_level']).to eq('private')
@@ -3493,31 +3700,23 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'sets container_registry_enabled' do
project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED)
- put(api("/projects/#{project.id}", user), params: { container_registry_enabled: true })
+ put(api(path, user), params: { container_registry_enabled: true })
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['container_registry_enabled']).to eq(true)
expect(project.reload.container_registry_access_level).to eq(ProjectFeature::ENABLED)
end
- it 'sets security_and_compliance_access_level', :aggregate_failures do
- put api("/projects/#{project.id}", user), params: { security_and_compliance_access_level: 'private' }
+ it 'sets security_and_compliance_access_level' do
+ put api(path, user), params: { security_and_compliance_access_level: 'private' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['security_and_compliance_access_level']).to eq('private')
expect(Project.find_by(path: project[:path]).security_and_compliance_access_level).to eq(ProjectFeature::PRIVATE)
end
- it 'sets operations_access_level', :aggregate_failures do
- put api("/projects/#{project.id}", user), params: { operations_access_level: 'private' }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['operations_access_level']).to eq('private')
- expect(Project.find_by(path: project[:path]).operations_access_level).to eq(ProjectFeature::PRIVATE)
- end
-
- it 'sets analytics_access_level', :aggregate_failures do
- put api("/projects/#{project.id}", user), params: { analytics_access_level: 'private' }
+ it 'sets analytics_access_level' do
+ put api(path, user), params: { analytics_access_level: 'private' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['analytics_access_level']).to eq('private')
@@ -3525,8 +3724,8 @@ RSpec.describe API::Projects, feature_category: :projects do
end
%i(releases_access_level environments_access_level feature_flags_access_level infrastructure_access_level monitor_access_level).each do |field|
- it "sets #{field}", :aggregate_failures do
- put api("/projects/#{project.id}", user), params: { field => 'private' }
+ it "sets #{field}" do
+ put api(path, user), params: { field => 'private' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response[field.to_s]).to eq('private')
@@ -3537,7 +3736,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'returns 400 when nothing sent' do
project_param = {}
- put api("/projects/#{project.id}", user), params: project_param
+ put api(path, user), params: project_param
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to match('at least one parameter must be provided')
@@ -3547,7 +3746,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'returns authentication error' do
project_param = { name: 'bar' }
- put api("/projects/#{project.id}"), params: project_param
+ put api(path), params: project_param
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -3593,7 +3792,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'does not update name to existing name' do
project_param = { name: project3.name }
- put api("/projects/#{project.id}", user), params: project_param
+ put api(path, user), params: project_param
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['name']).to eq(['has already been taken'])
@@ -3602,7 +3801,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'updates request_access_enabled' do
project_param = { request_access_enabled: false }
- put api("/projects/#{project.id}", user), params: project_param
+ put api(path, user), params: project_param
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['request_access_enabled']).to eq(false)
@@ -3623,7 +3822,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'updates default_branch' do
project_param = { default_branch: 'something_else' }
- put api("/projects/#{project.id}", user), params: project_param
+ put api(path, user), params: project_param
expect(response).to have_gitlab_http_status(:ok)
@@ -3712,7 +3911,7 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'updates restrict_user_defined_variables', :aggregate_failures do
+ it 'updates restrict_user_defined_variables' do
project_param = { restrict_user_defined_variables: true }
put api("/projects/#{project3.id}", user), params: project_param
@@ -3914,7 +4113,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'updates name' do
project_param = { name: 'bar' }
- put api("/projects/#{project.id}", user), params: project_param
+ put api(path, user), params: project_param
expect(response).to have_gitlab_http_status(:ok)
@@ -3989,7 +4188,7 @@ RSpec.describe API::Projects, feature_category: :projects do
merge_requests_enabled: true,
description: 'new description',
request_access_enabled: true }
- put api("/projects/#{project.id}", user3), params: project_param
+ put api(path, user3), params: project_param
expect(response).to have_gitlab_http_status(:forbidden)
end
end
@@ -4000,7 +4199,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'ignores visibility level restrictions' do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
- put api("/projects/#{project3.id}", admin), params: { visibility: 'internal' }
+ put api("/projects/#{project3.id}", admin, admin_mode: true), params: { visibility: 'internal' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['visibility']).to eq('internal')
@@ -4031,7 +4230,7 @@ RSpec.describe API::Projects, feature_category: :projects do
let(:admin) { create(:admin) }
it 'returns 400 when repository storage is unknown' do
- put(api("/projects/#{new_project.id}", admin), params: { repository_storage: unknown_storage })
+ put(api("/projects/#{new_project.id}", admin, admin_mode: true), params: { repository_storage: unknown_storage })
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['repository_storage_moves']).to eq(['is invalid'])
@@ -4042,7 +4241,7 @@ RSpec.describe API::Projects, feature_category: :projects do
expect do
Sidekiq::Testing.fake! do
- put(api("/projects/#{new_project.id}", admin), params: { repository_storage: 'test_second_storage' })
+ put(api("/projects/#{new_project.id}", admin, admin_mode: true), params: { repository_storage: 'test_second_storage' })
end
end.to change(Projects::UpdateRepositoryStorageWorker.jobs, :size).by(1)
@@ -4052,40 +4251,42 @@ RSpec.describe API::Projects, feature_category: :projects do
end
context 'when updating service desk' do
- subject { put(api("/projects/#{project.id}", user), params: { service_desk_enabled: true }) }
+ let(:params) { { service_desk_enabled: true } }
+
+ subject(:request) { put(api(path, user), params: params) }
before do
project.update!(service_desk_enabled: false)
- allow(::Gitlab::IncomingEmail).to receive(:enabled?).and_return(true)
+ allow(::Gitlab::Email::IncomingEmail).to receive(:enabled?).and_return(true)
end
it 'returns 200' do
- subject
+ request
expect(response).to have_gitlab_http_status(:ok)
end
it 'enables the service_desk' do
- expect { subject }.to change { project.reload.service_desk_enabled }.to(true)
+ expect { request }.to change { project.reload.service_desk_enabled }.to(true)
end
end
context 'when updating keep latest artifact' do
- subject { put(api("/projects/#{project.id}", user), params: { keep_latest_artifact: true }) }
+ subject(:request) { put(api(path, user), params: { keep_latest_artifact: true }) }
before do
project.update!(keep_latest_artifact: false)
end
it 'returns 200' do
- subject
+ request
expect(response).to have_gitlab_http_status(:ok)
end
it 'enables keep_latest_artifact' do
- expect { subject }.to change { project.reload.keep_latest_artifact }.to(true)
+ expect { request }.to change { project.reload.keep_latest_artifact }.to(true)
end
end
@@ -4131,9 +4332,11 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'POST /projects/:id/archive' do
+ let(:path) { "/projects/#{project.id}/archive" }
+
context 'on an unarchived project' do
it 'archives the project' do
- post api("/projects/#{project.id}/archive", user)
+ post api(path, user)
expect(response).to have_gitlab_http_status(:created)
expect(json_response['archived']).to be_truthy
@@ -4146,7 +4349,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'remains archived' do
- post api("/projects/#{project.id}/archive", user)
+ post api(path, user)
expect(response).to have_gitlab_http_status(:created)
expect(json_response['archived']).to be_truthy
@@ -4159,7 +4362,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'rejects the action' do
- post api("/projects/#{project.id}/archive", user3)
+ post api(path, user3)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -4167,9 +4370,11 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'POST /projects/:id/unarchive' do
+ let(:path) { "/projects/#{project.id}/unarchive" }
+
context 'on an unarchived project' do
it 'remains unarchived' do
- post api("/projects/#{project.id}/unarchive", user)
+ post api(path, user)
expect(response).to have_gitlab_http_status(:created)
expect(json_response['archived']).to be_falsey
@@ -4182,7 +4387,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'unarchives the project' do
- post api("/projects/#{project.id}/unarchive", user)
+ post api(path, user)
expect(response).to have_gitlab_http_status(:created)
expect(json_response['archived']).to be_falsey
@@ -4195,7 +4400,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'rejects the action' do
- post api("/projects/#{project.id}/unarchive", user3)
+ post api(path, user3)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -4203,9 +4408,11 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'POST /projects/:id/star' do
+ let(:path) { "/projects/#{project.id}/star" }
+
context 'on an unstarred project' do
it 'stars the project' do
- expect { post api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(1)
+ expect { post api(path, user) }.to change { project.reload.star_count }.by(1)
expect(response).to have_gitlab_http_status(:created)
expect(json_response['star_count']).to eq(1)
@@ -4219,7 +4426,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'does not modify the star count' do
- expect { post api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count }
+ expect { post api(path, user) }.not_to change { project.reload.star_count }
expect(response).to have_gitlab_http_status(:not_modified)
end
@@ -4227,6 +4434,8 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'POST /projects/:id/unstar' do
+ let(:path) { "/projects/#{project.id}/unstar" }
+
context 'on a starred project' do
before do
user.toggle_star(project)
@@ -4234,7 +4443,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'unstars the project' do
- expect { post api("/projects/#{project.id}/unstar", user) }.to change { project.reload.star_count }.by(-1)
+ expect { post api(path, user) }.to change { project.reload.star_count }.by(-1)
expect(response).to have_gitlab_http_status(:created)
expect(json_response['star_count']).to eq(0)
@@ -4243,7 +4452,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'on an unstarred project' do
it 'does not modify the star count' do
- expect { post api("/projects/#{project.id}/unstar", user) }.not_to change { project.reload.star_count }
+ expect { post api(path, user) }.not_to change { project.reload.star_count }
expect(response).to have_gitlab_http_status(:not_modified)
end
@@ -4251,9 +4460,13 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'GET /projects/:id/starrers' do
+ let(:path) { "/projects/#{public_project.id}/starrers" }
+ let(:public_project) { create(:project, :public) }
+ let(:private_user) { create(:user, private_profile: true) }
+
shared_examples_for 'project starrers response' do
it 'returns an array of starrers' do
- get api("/projects/#{public_project.id}/starrers", current_user)
+ get api(path, current_user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -4263,15 +4476,12 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'returns the proper security headers' do
- get api("/projects/#{public_project.id}/starrers", current_user)
+ get api(path, current_user)
expect(response).to include_security_headers
end
end
- let(:public_project) { create(:project, :public) }
- let(:private_user) { create(:user, private_profile: true) }
-
before do
user.update!(starred_projects: [public_project])
private_user.update!(starred_projects: [public_project])
@@ -4289,7 +4499,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'returns only starrers with a public profile' do
- get api("/projects/#{public_project.id}/starrers", nil)
+ get api(path, nil)
user_ids = json_response.map { |s| s['user']['id'] }
expect(user_ids).to include(user.id)
@@ -4303,7 +4513,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'returns current user with a private profile' do
- get api("/projects/#{public_project.id}/starrers", private_user)
+ get api(path, private_user)
user_ids = json_response.map { |s| s['user']['id'] }
expect(user_ids).to include(user.id, private_user.id)
@@ -4366,9 +4576,16 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'DELETE /projects/:id' do
+ let(:path) { "/projects/#{project.id}" }
+
+ it_behaves_like 'DELETE request permissions for admin mode' do
+ let(:success_status_code) { :accepted }
+ let(:failed_status_code) { :not_found }
+ end
+
context 'when authenticated as user' do
it 'removes project' do
- delete api("/projects/#{project.id}", user)
+ delete api(path, user)
expect(response).to have_gitlab_http_status(:accepted)
expect(json_response['message']).to eql('202 Accepted')
@@ -4376,13 +4593,13 @@ RSpec.describe API::Projects, feature_category: :projects do
it_behaves_like '412 response' do
let(:success_status) { 202 }
- let(:request) { api("/projects/#{project.id}", user) }
+ subject(:request) { api(path, user) }
end
it 'does not remove a project if not an owner' do
user3 = create(:user)
project.add_developer(user3)
- delete api("/projects/#{project.id}", user3)
+ delete api(path, user3)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -4392,27 +4609,27 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'does not remove a project not attached to user' do
- delete api("/projects/#{project.id}", user2)
+ delete api(path, user2)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when authenticated as admin' do
it 'removes any existing project' do
- delete api("/projects/#{project.id}", admin)
+ delete api("/projects/#{project.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:accepted)
expect(json_response['message']).to eql('202 Accepted')
end
it 'does not remove a non existing project' do
- delete api("/projects/#{non_existing_record_id}", admin)
+ delete api("/projects/#{non_existing_record_id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
it_behaves_like '412 response' do
let(:success_status) { 202 }
- let(:request) { api("/projects/#{project.id}", admin) }
+ subject(:request) { api("/projects/#{project.id}", admin, admin_mode: true) }
end
end
end
@@ -4422,6 +4639,8 @@ RSpec.describe API::Projects, feature_category: :projects do
create(:project, :repository, creator: user, namespace: user.namespace)
end
+ let(:path) { "/projects/#{project.id}/fork" }
+
let(:project2) do
create(:project, :repository, creator: user, namespace: user.namespace)
end
@@ -4438,9 +4657,14 @@ RSpec.describe API::Projects, feature_category: :projects do
project2.add_reporter(user2)
end
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { {} }
+ let(:failed_status_code) { :not_found }
+ end
+
context 'when authenticated' do
it 'forks if user has sufficient access to project' do
- post api("/projects/#{project.id}/fork", user2)
+ post api(path, user2)
expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq(project.name)
@@ -4453,7 +4677,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'forks if user is admin' do
- post api("/projects/#{project.id}/fork", admin)
+ post api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq(project.name)
@@ -4467,7 +4691,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'fails on missing project access for the project to fork' do
new_user = create(:user)
- post api("/projects/#{project.id}/fork", new_user)
+ post api(path, new_user)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Project Not Found')
@@ -4492,41 +4716,41 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'forks with explicit own user namespace id' do
- post api("/projects/#{project.id}/fork", user2), params: { namespace: user2.namespace.id }
+ post api(path, user2), params: { namespace: user2.namespace.id }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['owner']['id']).to eq(user2.id)
end
it 'forks with explicit own user name as namespace' do
- post api("/projects/#{project.id}/fork", user2), params: { namespace: user2.username }
+ post api(path, user2), params: { namespace: user2.username }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['owner']['id']).to eq(user2.id)
end
it 'forks to another user when admin' do
- post api("/projects/#{project.id}/fork", admin), params: { namespace: user2.username }
+ post api(path, admin, admin_mode: true), params: { namespace: user2.username }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['owner']['id']).to eq(user2.id)
end
it 'fails if trying to fork to another user when not admin' do
- post api("/projects/#{project.id}/fork", user2), params: { namespace: admin.namespace.id }
+ post api(path, user2), params: { namespace: admin.namespace.id }
expect(response).to have_gitlab_http_status(:not_found)
end
it 'fails if trying to fork to non-existent namespace' do
- post api("/projects/#{project.id}/fork", user2), params: { namespace: non_existing_record_id }
+ post api(path, user2), params: { namespace: non_existing_record_id }
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Namespace Not Found')
end
it 'forks to owned group' do
- post api("/projects/#{project.id}/fork", user2), params: { namespace: group2.name }
+ post api(path, user2), params: { namespace: group2.name }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['namespace']['name']).to eq(group2.name)
@@ -4543,7 +4767,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'and namespace_id is specified alone' do
before do
- post api("/projects/#{project.id}/fork", user2), params: { namespace_id: user2.namespace.id }
+ post api(path, user2), params: { namespace_id: user2.namespace.id }
end
it_behaves_like 'forking to specified namespace_id'
@@ -4551,7 +4775,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'and namespace_id and namespace are both specified' do
before do
- post api("/projects/#{project.id}/fork", user2), params: { namespace_id: user2.namespace.id, namespace: admin.namespace.id }
+ post api(path, user2), params: { namespace_id: user2.namespace.id, namespace: admin.namespace.id }
end
it_behaves_like 'forking to specified namespace_id'
@@ -4559,7 +4783,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'and namespace_id and namespace_path are both specified' do
before do
- post api("/projects/#{project.id}/fork", user2), params: { namespace_id: user2.namespace.id, namespace_path: admin.namespace.path }
+ post api(path, user2), params: { namespace_id: user2.namespace.id, namespace_path: admin.namespace.path }
end
it_behaves_like 'forking to specified namespace_id'
@@ -4577,7 +4801,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'and namespace_path is specified alone' do
before do
- post api("/projects/#{project.id}/fork", user2), params: { namespace_path: user2.namespace.path }
+ post api(path, user2), params: { namespace_path: user2.namespace.path }
end
it_behaves_like 'forking to specified namespace_path'
@@ -4585,7 +4809,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'and namespace_path and namespace are both specified' do
before do
- post api("/projects/#{project.id}/fork", user2), params: { namespace_path: user2.namespace.path, namespace: admin.namespace.path }
+ post api(path, user2), params: { namespace_path: user2.namespace.path, namespace: admin.namespace.path }
end
it_behaves_like 'forking to specified namespace_path'
@@ -4594,7 +4818,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'forks to owned subgroup' do
full_path = "#{group2.path}/#{group3.path}"
- post api("/projects/#{project.id}/fork", user2), params: { namespace: full_path }
+ post api(path, user2), params: { namespace: full_path }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['namespace']['name']).to eq(group3.name)
@@ -4602,21 +4826,21 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'fails to fork to not owned group' do
- post api("/projects/#{project.id}/fork", user2), params: { namespace: group.name }
+ post api(path, user2), params: { namespace: group.name }
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq("404 Target Namespace Not Found")
end
it 'forks to not owned group when admin' do
- post api("/projects/#{project.id}/fork", admin), params: { namespace: group.name }
+ post api(path, admin, admin_mode: true), params: { namespace: group.name }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['namespace']['name']).to eq(group.name)
end
it 'accepts a path for the target project' do
- post api("/projects/#{project.id}/fork", user2), params: { path: 'foobar' }
+ post api(path, user2), params: { path: 'foobar' }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq(project.name)
@@ -4629,7 +4853,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'fails to fork if path is already taken' do
- post api("/projects/#{project.id}/fork", user2), params: { path: 'foobar' }
+ post api(path, user2), params: { path: 'foobar' }
post api("/projects/#{project2.id}/fork", user2), params: { path: 'foobar' }
expect(response).to have_gitlab_http_status(:conflict)
@@ -4637,7 +4861,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'accepts custom parameters for the target project' do
- post api("/projects/#{project.id}/fork", user2),
+ post api(path, user2),
params: {
name: 'My Random Project',
description: 'A description',
@@ -4659,7 +4883,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'fails to fork if name is already taken' do
- post api("/projects/#{project.id}/fork", user2), params: { name: 'My Random Project' }
+ post api(path, user2), params: { name: 'My Random Project' }
post api("/projects/#{project2.id}/fork", user2), params: { name: 'My Random Project' }
expect(response).to have_gitlab_http_status(:conflict)
@@ -4667,7 +4891,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'forks to the same namespace with alternative path and name' do
- post api("/projects/#{project.id}/fork", user), params: { path: 'path_2', name: 'name_2' }
+ post api(path, user), params: { path: 'path_2', name: 'name_2' }
expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq('name_2')
@@ -4679,7 +4903,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'fails to fork to the same namespace without alternative path and name' do
- post api("/projects/#{project.id}/fork", user)
+ post api(path, user)
expect(response).to have_gitlab_http_status(:conflict)
expect(json_response['message']['path']).to eq(['has already been taken'])
@@ -4687,7 +4911,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'fails to fork with an unknown visibility level' do
- post api("/projects/#{project.id}/fork", user2), params: { visibility: 'something' }
+ post api(path, user2), params: { visibility: 'something' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('visibility does not have a valid value')
@@ -4696,7 +4920,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'when unauthenticated' do
it 'returns authentication error' do
- post api("/projects/#{project.id}/fork")
+ post api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
expect(json_response['message']).to eq('401 Unauthorized')
@@ -4710,7 +4934,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'denies project to be forked' do
- post api("/projects/#{project.id}/fork", admin)
+ post api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -4720,8 +4944,9 @@ RSpec.describe API::Projects, feature_category: :projects do
describe 'POST /projects/:id/housekeeping' do
let(:housekeeping) { Repositories::HousekeepingService.new(project) }
let(:params) { {} }
+ let(:path) { "/projects/#{project.id}/housekeeping" }
- subject { post api("/projects/#{project.id}/housekeeping", user), params: params }
+ subject(:request) { post api(path, user), params: params }
before do
allow(Repositories::HousekeepingService).to receive(:new).with(project, :eager).and_return(housekeeping)
@@ -4731,7 +4956,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'starts the housekeeping process' do
expect(housekeeping).to receive(:execute).once
- subject
+ request
expect(response).to have_gitlab_http_status(:created)
end
@@ -4746,7 +4971,7 @@ RSpec.describe API::Projects, feature_category: :projects do
message: "Housekeeping task: eager"
))
- subject
+ request
end
context 'when requesting prune' do
@@ -4756,7 +4981,7 @@ RSpec.describe API::Projects, feature_category: :projects do
expect(Repositories::HousekeepingService).to receive(:new).with(project, :prune).and_return(housekeeping)
expect(housekeeping).to receive(:execute).once
- subject
+ request
expect(response).to have_gitlab_http_status(:created)
end
@@ -4768,7 +4993,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'responds with bad_request' do
expect(Repositories::HousekeepingService).not_to receive(:new)
- subject
+ request
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -4778,7 +5003,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'returns conflict' do
expect(housekeeping).to receive(:execute).once.and_raise(Repositories::HousekeepingService::LeaseTaken)
- subject
+ request
expect(response).to have_gitlab_http_status(:conflict)
expect(json_response['message']).to match(/Somebody already triggered housekeeping for this resource/)
@@ -4792,7 +5017,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'returns forbidden error' do
- post api("/projects/#{project.id}/housekeeping", user3)
+ post api(path, user3)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -4800,7 +5025,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'when unauthenticated' do
it 'returns authentication error' do
- post api("/projects/#{project.id}/housekeeping")
+ post api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -4809,6 +5034,7 @@ RSpec.describe API::Projects, feature_category: :projects do
describe 'POST /projects/:id/repository_size' do
let(:update_statistics_service) { Projects::UpdateStatisticsService.new(project, nil, statistics: [:repository_size, :lfs_objects_size]) }
+ let(:path) { "/projects/#{project.id}/repository_size" }
before do
allow(Projects::UpdateStatisticsService).to receive(:new).with(project, nil, statistics: [:repository_size, :lfs_objects_size]).and_return(update_statistics_service)
@@ -4818,7 +5044,7 @@ RSpec.describe API::Projects, feature_category: :projects do
it 'starts the housekeeping process' do
expect(update_statistics_service).to receive(:execute).once
- post api("/projects/#{project.id}/repository_size", user)
+ post api(path, user)
expect(response).to have_gitlab_http_status(:created)
end
@@ -4830,7 +5056,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'returns forbidden error' do
- post api("/projects/#{project.id}/repository_size", user3)
+ post api(path, user3)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -4838,7 +5064,7 @@ RSpec.describe API::Projects, feature_category: :projects do
context 'when unauthenticated' do
it 'returns authentication error' do
- post api("/projects/#{project.id}/repository_size")
+ post api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -4846,31 +5072,33 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'PUT /projects/:id/transfer' do
+ let(:path) { "/projects/#{project.id}/transfer" }
+
context 'when authenticated as owner' do
let(:group) { create :group }
it 'transfers the project to the new namespace' do
group.add_owner(user)
- put api("/projects/#{project.id}/transfer", user), params: { namespace: group.id }
+ put api(path, user), params: { namespace: group.id }
expect(response).to have_gitlab_http_status(:ok)
end
it 'fails when transferring to a non owned namespace' do
- put api("/projects/#{project.id}/transfer", user), params: { namespace: group.id }
+ put api(path, user), params: { namespace: group.id }
expect(response).to have_gitlab_http_status(:not_found)
end
it 'fails when transferring to an unknown namespace' do
- put api("/projects/#{project.id}/transfer", user), params: { namespace: 'unknown' }
+ put api(path, user), params: { namespace: 'unknown' }
expect(response).to have_gitlab_http_status(:not_found)
end
it 'fails on missing namespace' do
- put api("/projects/#{project.id}/transfer", user)
+ put api(path, user)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -4885,7 +5113,7 @@ RSpec.describe API::Projects, feature_category: :projects do
let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) }
it 'fails transferring the project to the target namespace' do
- put api("/projects/#{project.id}/transfer", user), params: { namespace: group.id }
+ put api(path, user), params: { namespace: group.id }
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -4988,16 +5216,20 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'GET /projects/:id/storage' do
+ let(:path) { "/projects/#{project.id}/storage" }
+
+ it_behaves_like 'GET request permissions for admin mode'
+
context 'when unauthenticated' do
it 'does not return project storage data' do
- get api("/projects/#{project.id}/storage")
+ get api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
it 'returns project storage data when user is admin' do
- get api("/projects/#{project.id}/storage", create(:admin))
+ get api(path, create(:admin), admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['project_id']).to eq(project.id)
@@ -5007,7 +5239,7 @@ RSpec.describe API::Projects, feature_category: :projects do
end
it 'does not return project storage data when user is not admin' do
- get api("/projects/#{project.id}/storage", user3)
+ get api(path, user3)
expect(response).to have_gitlab_http_status(:forbidden)
end
diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb
index 8e8a25a8dc2..04d5f7ac20a 100644
--- a/spec/requests/api/protected_branches_spec.rb
+++ b/spec/requests/api/protected_branches_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe API::ProtectedBranches, feature_category: :source_code_management do
let_it_be_with_reload(:project) { create(:project, :repository) }
let_it_be(:maintainer) { create(:user) }
+ let_it_be(:developer) { create(:user) }
let_it_be(:guest) { create(:user) }
let(:protected_name) { 'feature' }
@@ -16,12 +17,14 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management
before_all do
project.add_maintainer(maintainer)
+ project.add_developer(developer)
project.add_guest(guest)
end
describe "GET /projects/:id/protected_branches" do
let(:params) { {} }
let(:route) { "/projects/#{project.id}/protected_branches" }
+ let(:expected_branch_names) { project.protected_branches.map { |x| x['name'] } }
shared_examples_for 'protected branches' do
it 'returns the protected branches' do
@@ -39,9 +42,7 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management
let(:user) { maintainer }
context 'when search param is not present' do
- it_behaves_like 'protected branches' do
- let(:expected_branch_names) { project.protected_branches.map { |x| x['name'] } }
- end
+ it_behaves_like 'protected branches'
end
context 'when search param is present' do
@@ -53,6 +54,12 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management
end
end
+ context 'when authenticated as a developer' do
+ let(:user) { developer }
+
+ it_behaves_like 'protected branches'
+ end
+
context 'when authenticated as a guest' do
let(:user) { guest }
@@ -103,6 +110,27 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management
it_behaves_like 'protected branch'
end
+
+ context 'when a deploy key is present' do
+ let(:deploy_key) do
+ create(:deploy_key, deploy_keys_projects: [create(:deploy_keys_project, :write_access, project: project)])
+ end
+
+ it 'returns deploy key information' do
+ create(:protected_branch_push_access_level, protected_branch: protected_branch, deploy_key: deploy_key)
+ get api(route, user)
+
+ expect(json_response['push_access_levels']).to include(
+ a_hash_including('access_level_description' => 'Deploy key', 'deploy_key_id' => deploy_key.id)
+ )
+ end
+ end
+ end
+
+ context 'when authenticated as a developer' do
+ let(:user) { developer }
+
+ it_behaves_like 'protected branch'
end
context 'when authenticated as a guest' do
@@ -243,10 +271,20 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management
end
end
+ context 'when authenticated as a developer' do
+ let(:user) { developer }
+
+ it "returns a 403 error" do
+ post post_endpoint, params: { name: branch_name }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
context 'when authenticated as a guest' do
let(:user) { guest }
- it "returns a 403 error if guest" do
+ it "returns a 403 error" do
post post_endpoint, params: { name: branch_name }
expect(response).to have_gitlab_http_status(:forbidden)
@@ -266,6 +304,15 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management
end.to change { protected_branch.reload.allow_force_push }.from(false).to(true)
expect(response).to have_gitlab_http_status(:ok)
end
+
+ context 'when allow_force_push is not set' do
+ it 'responds with a bad request error' do
+ patch api(route, user), params: { allow_force_push: nil }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq 'allow_force_push is empty'
+ end
+ end
end
context 'when returned protected branch is invalid' do
@@ -286,6 +333,16 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management
end
end
+ context 'when authenticated as a developer' do
+ let(:user) { developer }
+
+ it "returns a 403 error" do
+ patch api(route, user), params: { allow_force_push: true }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
context 'when authenticated as a guest' do
let(:user) { guest }
@@ -298,42 +355,65 @@ RSpec.describe API::ProtectedBranches, feature_category: :source_code_management
end
describe "DELETE /projects/:id/protected_branches/unprotect/:branch" do
- let(:user) { maintainer }
let(:delete_endpoint) { api("/projects/#{project.id}/protected_branches/#{branch_name}", user) }
- it "unprotects a single branch" do
- delete delete_endpoint
+ context "when authenticated as a maintainer" do
+ let(:user) { maintainer }
- expect(response).to have_gitlab_http_status(:no_content)
- end
+ it "unprotects a single branch" do
+ delete delete_endpoint
- it_behaves_like '412 response' do
- let(:request) { delete_endpoint }
- end
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+
+ it_behaves_like '412 response' do
+ let(:request) { delete_endpoint }
+ end
+
+ it "returns 404 if branch does not exist" do
+ delete api("/projects/#{project.id}/protected_branches/barfoo", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'when a policy restricts rule deletion' do
+ it "prevents deletion of the protected branch rule" do
+ disallow(:destroy_protected_branch, protected_branch)
- it "returns 404 if branch does not exist" do
- delete api("/projects/#{project.id}/protected_branches/barfoo", user)
+ delete delete_endpoint
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when branch has a wildcard in its name' do
+ let(:protected_name) { 'feature*' }
+
+ it "unprotects a wildcard branch" do
+ delete delete_endpoint
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
end
- context 'when a policy restricts rule deletion' do
- it "prevents deletion of the protected branch rule" do
- disallow(:destroy_protected_branch, protected_branch)
+ context 'when authenticated as a developer' do
+ let(:user) { developer }
+ it "returns a 403 error" do
delete delete_endpoint
expect(response).to have_gitlab_http_status(:forbidden)
end
end
- context 'when branch has a wildcard in its name' do
- let(:protected_name) { 'feature*' }
+ context 'when authenticated as a guest' do
+ let(:user) { guest }
- it "unprotects a wildcard branch" do
+ it "returns a 403 error" do
delete delete_endpoint
- expect(response).to have_gitlab_http_status(:no_content)
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
diff --git a/spec/requests/api/protected_tags_spec.rb b/spec/requests/api/protected_tags_spec.rb
index 5b128d4ec9e..c6398e624f8 100644
--- a/spec/requests/api/protected_tags_spec.rb
+++ b/spec/requests/api/protected_tags_spec.rb
@@ -84,6 +84,21 @@ RSpec.describe API::ProtectedTags, feature_category: :source_code_management do
it_behaves_like 'protected tag'
end
+
+ context 'when a deploy key is present' do
+ let(:deploy_key) do
+ create(:deploy_key, deploy_keys_projects: [create(:deploy_keys_project, :write_access, project: project)])
+ end
+
+ it 'returns deploy key information' do
+ create(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: deploy_key)
+ get api(route, user)
+
+ expect(json_response['create_access_levels']).to include(
+ a_hash_including('access_level_description' => 'Deploy key', 'deploy_key_id' => deploy_key.id)
+ )
+ end
+ end
end
context 'when authenticated as a guest' do
diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb
index 978d4f72a4a..0b2641b062c 100644
--- a/spec/requests/api/pypi_packages_spec.rb
+++ b/spec/requests/api/pypi_packages_spec.rb
@@ -14,10 +14,18 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_pypi_user' } }
+ let(:snowplow_gitlab_standard_context) { snowplow_context }
let(:headers) { {} }
+ def snowplow_context(user_role: :developer)
+ if user_role == :anonymous
+ { project: project, namespace: project.namespace, property: 'i_package_pypi_user' }
+ else
+ { project: project, namespace: project.namespace, property: 'i_package_pypi_user', user: user }
+ end
+ end
+
context 'simple index API endpoint' do
let_it_be(:package) { create(:pypi_package, project: project) }
let_it_be(:package2) { create(:pypi_package, project: project) }
@@ -26,7 +34,6 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do
describe 'GET /api/v4/groups/:id/-/packages/pypi/simple' do
let(:url) { "/groups/#{group.id}/-/packages/pypi/simple" }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_pypi_user' } }
it_behaves_like 'pypi simple index API endpoint'
it_behaves_like 'rejects PyPI access with unknown group id'
@@ -82,13 +89,13 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do
context 'simple package API endpoint' do
let_it_be(:package) { create(:pypi_package, project: project) }
- let(:snowplow_gitlab_standard_context) { { project: nil, namespace: group, property: 'i_package_pypi_user' } }
subject { get api(url), headers: headers }
describe 'GET /api/v4/groups/:id/-/packages/pypi/simple/:package_name' do
let(:package_name) { package.name }
let(:url) { "/groups/#{group.id}/-/packages/pypi/simple/#{package_name}" }
+ let(:snowplow_context) { { project: nil, namespace: project.namespace, property: 'i_package_pypi_user' } }
it_behaves_like 'pypi simple API endpoint'
it_behaves_like 'rejects PyPI access with unknown group id'
@@ -126,7 +133,7 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do
describe 'GET /api/v4/projects/:id/packages/pypi/simple/:package_name' do
let(:package_name) { package.name }
let(:url) { "/projects/#{project.id}/packages/pypi/simple/#{package_name}" }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_pypi_user' } }
+ let(:snowplow_context) { { project: project, namespace: project.namespace, property: 'i_package_pypi_user' } }
it_behaves_like 'pypi simple API endpoint'
it_behaves_like 'rejects PyPI access with unknown project id'
@@ -242,6 +249,13 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
let(:headers) { user_headers.merge(workhorse_headers) }
+ let(:snowplow_gitlab_standard_context) do
+ if user_role == :anonymous || (visibility_level == :public && !user_token)
+ { project: project, namespace: project.namespace, property: 'i_package_pypi_user' }
+ else
+ { project: project, namespace: project.namespace, property: 'i_package_pypi_user', user: user }
+ end
+ end
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
@@ -379,6 +393,14 @@ RSpec.describe API::PypiPackages, feature_category: :package_registry do
let_it_be(:package_name) { 'Dummy-Package' }
let_it_be(:package) { create(:pypi_package, project: project, name: package_name, version: '1.0.0') }
+ let(:snowplow_gitlab_standard_context) do
+ if user_role == :anonymous || (visibility_level == :public && !user_token)
+ { project: project, namespace: project.namespace, property: 'i_package_pypi_user' }
+ else
+ { project: project, namespace: project.namespace, property: 'i_package_pypi_user', user: user }
+ end
+ end
+
subject { get api(url), headers: headers }
describe 'GET /api/v4/groups/:id/-/packages/pypi/files/:sha256/*file_identifier' do
diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb
index 462cc1e3b5d..b8c10de2302 100644
--- a/spec/requests/api/release/links_spec.rb
+++ b/spec/requests/api/release/links_spec.rb
@@ -174,7 +174,7 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do
specify do
get api("/projects/#{project.id}/releases/v0.1/assets/links/#{link.id}", maintainer)
- expect(json_response['direct_asset_url']).to eq("http://localhost/#{project.namespace.path}/#{project.name}/-/releases/#{release.tag}/downloads/bin/bigfile.exe")
+ expect(json_response['direct_asset_url']).to eq("http://localhost/#{project.full_path}/-/releases/#{release.tag}/downloads/bin/bigfile.exe")
end
end
@@ -377,12 +377,21 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do
expect(response).to match_response_schema('release/link')
end
+ context 'when params are invalid' do
+ it 'returns 400 error' do
+ put api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}", maintainer),
+ params: params.merge(url: 'wrong_url')
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
context 'when using `direct_asset_path`' do
it 'updates the release link' do
put api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}", maintainer),
params: params.merge(direct_asset_path: '/binaries/awesome-app.msi')
- expect(json_response['direct_asset_url']).to eq("http://localhost/#{project.namespace.path}/#{project.name}/-/releases/#{release.tag}/downloads/binaries/awesome-app.msi")
+ expect(json_response['direct_asset_url']).to eq("http://localhost/#{project.full_path}/-/releases/#{release.tag}/downloads/binaries/awesome-app.msi")
end
end
@@ -534,6 +543,21 @@ RSpec.describe API::Release::Links, feature_category: :release_orchestration do
end
end
+ context 'when destroy process fails' do
+ before do
+ allow_next_instance_of(::Releases::Links::DestroyService) do |service|
+ allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'error'))
+ end
+ end
+
+ it_behaves_like '400 response' do
+ let(:message) { 'error' }
+ let(:request) do
+ delete api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}", maintainer)
+ end
+ end
+ end
+
context 'when there are no corresponding release link' do
let!(:release_link) {}
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index c3f99872cef..0b5cc3611bd 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Releases, feature_category: :release_orchestration do
+RSpec.describe API::Releases, :aggregate_failures, feature_category: :release_orchestration do
let(:project) { create(:project, :repository, :private) }
let(:maintainer) { create(:user) }
let(:reporter) { create(:user) }
@@ -422,22 +422,6 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do
.to eq('release-18.04.dmg')
expect(json_response['assets']['links'].first['url'])
.to eq('https://my-external-hosting.example.com/scrambled-url/app.zip')
- expect(json_response['assets']['links'].first['external'])
- .to be_truthy
- end
-
- context 'when link is internal' do
- let(:url) do
- "#{project.web_url}/-/jobs/artifacts/v11.6.0-rc4/download?" \
- "job=rspec-mysql+41%2F50"
- end
-
- it 'has external false' do
- get api("/projects/#{project.id}/releases/v0.1", maintainer)
-
- expect(json_response['assets']['links'].first['external'])
- .to be_falsy
- end
end
end
@@ -480,7 +464,7 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do
end
context 'when specified tag is not found in the project' do
- it 'returns 404 for maintater' do
+ it 'returns 404 for maintainer' do
get api("/projects/#{project.id}/releases/non_exist_tag", maintainer)
expect(response).to have_gitlab_http_status(:not_found)
@@ -1665,7 +1649,11 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do
let_it_be(:release2) { create(:release, project: project2) }
let_it_be(:release3) { create(:release, project: project3) }
- context 'when authenticated as owner' do
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:path) { "/groups/#{group1.id}/releases" }
+ end
+
+ context 'when authenticated as owner', :enable_admin_mode do
it 'gets releases from all projects in the group' do
get api("/groups/#{group1.id}/releases", admin)
@@ -1715,9 +1703,14 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do
context 'with subgroups' do
let(:group) { create(:group) }
- it 'include_subgroups avoids N+1 queries' do
+ subject { get api("/groups/#{group.id}/releases", admin, admin_mode: true), params: query_params.merge({ include_subgroups: true }) }
+
+ it 'include_subgroups avoids N+1 queries', :use_sql_query_cache do
+ subject
+ expect(response).to have_gitlab_http_status(:ok)
+
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api("/groups/#{group.id}/releases", admin), params: query_params.merge({ include_subgroups: true })
+ subject
end.count
subgroups = create_list(:group, 10, parent: group1)
@@ -1725,7 +1718,7 @@ RSpec.describe API::Releases, feature_category: :release_orchestration do
create_list(:release, 10, project: projects[0], author: admin)
expect do
- get api("/groups/#{group.id}/releases", admin), params: query_params.merge({ include_subgroups: true })
+ subject
end.not_to exceed_all_query_limit(control_count)
end
end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index be26fe24061..8853eff0b3e 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -236,7 +236,6 @@ RSpec.describe API::Repositories, feature_category: :source_code_management do
get api(route, current_user)
expect(response.headers["Cache-Control"]).to eq("max-age=0, private, must-revalidate, no-store, no-cache")
- expect(response.headers["Pragma"]).to eq("no-cache")
expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT")
end
diff --git a/spec/requests/api/resource_access_tokens_spec.rb b/spec/requests/api/resource_access_tokens_spec.rb
index 6a89e9a56df..ce05fa2b383 100644
--- a/spec/requests/api/resource_access_tokens_spec.rb
+++ b/spec/requests/api/resource_access_tokens_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe API::ResourceAccessTokens, feature_category: :authentication_and_authorization do
+RSpec.describe API::ResourceAccessTokens, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:user_non_priviledged) { create(:user) }
@@ -336,13 +336,33 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :authentication_and_
context "when 'expires_at' is not set" do
let(:expires_at) { nil }
- it "creates a #{source_type} access token with the params", :aggregate_failures do
- create_token
+ context 'when default_pat_expiration feature flag is true' do
+ it "creates a #{source_type} access token with the default expires_at value", :aggregate_failures do
+ freeze_time do
+ create_token
+ expires_at = PersonalAccessToken::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS.days.from_now
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response["name"]).to eq("test")
+ expect(json_response["scopes"]).to eq(["api"])
+ expect(json_response["expires_at"]).to eq(expires_at.to_date.iso8601)
+ end
+ end
+ end
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response["name"]).to eq("test")
- expect(json_response["scopes"]).to eq(["api"])
- expect(json_response["expires_at"]).to eq(nil)
+ context 'when default_pat_expiration feature flag is false' do
+ before do
+ stub_feature_flags(default_pat_expiration: false)
+ end
+
+ it "creates a #{source_type} access token with the params", :aggregate_failures do
+ create_token
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response["name"]).to eq("test")
+ expect(json_response["scopes"]).to eq(["api"])
+ expect(json_response["expires_at"]).to eq(nil)
+ end
end
end
@@ -468,10 +488,86 @@ RSpec.describe API::ResourceAccessTokens, feature_category: :authentication_and_
end
end
end
+
+ context "POST #{source_type}s/:id/access_tokens/:token_id/rotate" do
+ let_it_be(:project_bot) { create(:user, :project_bot) }
+ let_it_be(:token) { create(:personal_access_token, user: project_bot) }
+ let_it_be(:resource_id) { resource.id }
+ let_it_be(:token_id) { token.id }
+
+ let(:path) { "/#{source_type}s/#{resource_id}/access_tokens/#{token_id}/rotate" }
+
+ before do
+ resource.add_maintainer(project_bot)
+ resource.add_owner(user)
+ end
+
+ subject(:rotate_token) { post api(path, user) }
+
+ it "allows owner to rotate token", :freeze_time do
+ rotate_token
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['token']).not_to eq(token.token)
+ expect(json_response['expires_at']).to eq((Date.today + 1.week).to_s)
+ end
+
+ context 'without permission' do
+ it 'returns an error message' do
+ another_user = create(:user)
+ resource.add_developer(another_user)
+
+ post api(path, another_user)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when service raises an error' do
+ let(:error_message) { 'boom!' }
+
+ before do
+ allow_next_instance_of(PersonalAccessTokens::RotateService) do |service|
+ allow(service).to receive(:execute).and_return(ServiceResponse.error(message: error_message))
+ end
+ end
+
+ it 'returns the same error message' do
+ rotate_token
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq("400 Bad request - #{error_message}")
+ end
+ end
+
+ context 'when token does not exist' do
+ let(:invalid_path) { "/#{source_type}s/#{resource_id}/access_tokens/#{non_existing_record_id}/rotate" }
+
+ context 'for non-admin user' do
+ it 'returns unauthorized' do
+ user = create(:user)
+ resource.add_developer(user)
+
+ post api(invalid_path, user)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'for admin user', :enable_admin_mode do
+ it 'returns not found' do
+ admin = create(:admin)
+ post api(invalid_path, admin)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
end
context 'when the resource is a project' do
- let_it_be(:resource) { create(:project) }
+ let_it_be(:resource) { create(:project, group: create(:group)) }
let_it_be(:other_resource) { create(:project) }
let_it_be(:unknown_resource) { create(:project) }
diff --git a/spec/requests/api/rubygem_packages_spec.rb b/spec/requests/api/rubygem_packages_spec.rb
index 34cf6033811..1774b43ccb3 100644
--- a/spec/requests/api/rubygem_packages_spec.rb
+++ b/spec/requests/api/rubygem_packages_spec.rb
@@ -8,6 +8,14 @@ RSpec.describe API::RubygemPackages, feature_category: :package_registry do
using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:project) { create(:project) }
+ let(:tokens) do
+ {
+ personal_access_token: personal_access_token.token,
+ deploy_token: deploy_token.token,
+ job_token: job.token
+ }
+ end
+
let_it_be(:personal_access_token) { create(:personal_access_token) }
let_it_be(:user) { personal_access_token.user }
let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
@@ -15,14 +23,14 @@ RSpec.describe API::RubygemPackages, feature_category: :package_registry do
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let_it_be(:headers) { {} }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_rubygems_user' } }
+ let(:snowplow_gitlab_standard_context) { snowplow_context }
- let(:tokens) do
- {
- personal_access_token: personal_access_token.token,
- deploy_token: deploy_token.token,
- job_token: job.token
- }
+ def snowplow_context(user_role: :developer)
+ if user_role == :anonymous
+ { project: project, namespace: project.namespace, property: 'i_package_rubygems_user' }
+ else
+ { project: project, namespace: project.namespace, property: 'i_package_rubygems_user', user: user }
+ end
end
shared_examples 'when feature flag is disabled' do
@@ -164,7 +172,13 @@ RSpec.describe API::RubygemPackages, feature_category: :package_registry do
with_them do
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
let(:headers) { user_role == :anonymous ? {} : { 'HTTP_AUTHORIZATION' => token } }
- let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_rubygems_user' } }
+ let(:snowplow_gitlab_standard_context) do
+ if token_type == :deploy_token
+ snowplow_context.merge(user: deploy_token)
+ else
+ snowplow_context(user_role: user_role)
+ end
+ end
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility.to_s))
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 035f53db12e..a315bca58d1 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Search, feature_category: :global_search do
+RSpec.describe API::Search, :clean_gitlab_redis_rate_limiting, feature_category: :global_search do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project, reload: true) { create(:project, :wiki_repo, :public, name: 'awesome project', group: group) }
@@ -10,8 +10,6 @@ RSpec.describe API::Search, feature_category: :global_search do
before do
allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).and_return(0)
- allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000)
- allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000)
end
shared_examples 'response is correct' do |schema:, size: 1|
@@ -141,7 +139,7 @@ RSpec.describe API::Search, feature_category: :global_search do
end
end
- context 'when DB timeouts occur from global searches', :aggregate_errors do
+ context 'when DB timeouts occur from global searches', :aggregate_failures do
%w(
issues
merge_requests
@@ -174,6 +172,23 @@ RSpec.describe API::Search, feature_category: :global_search do
end
end
+ context 'when there is a search error' do
+ let(:results) { instance_double('Gitlab::SearchResults', failed?: true, error: 'failed to parse query') }
+
+ before do
+ allow_next_instance_of(SearchService) do |service|
+ allow(service).to receive(:search_objects).and_return([])
+ allow(service).to receive(:search_results).and_return(results)
+ end
+ end
+
+ it 'returns 400 error' do
+ get api(endpoint, user), params: { scope: 'issues', search: 'expected to fail' }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
context 'with correct params' do
context 'for projects scope' do
before do
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 4d85849cff3..3f66cbaf2b7 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, feature_category: :not_owned do
+RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, feature_category: :shared do
let(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
@@ -66,6 +66,16 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
expect(json_response['jira_connect_application_key']).to eq(nil)
expect(json_response['jira_connect_proxy_url']).to eq(nil)
expect(json_response['user_defaults_to_private_profile']).to eq(false)
+ expect(json_response['default_syntax_highlighting_theme']).to eq(1)
+ expect(json_response['projects_api_rate_limit_unauthenticated']).to eq(400)
+ expect(json_response['silent_mode_enabled']).to be(false)
+ expect(json_response['slack_app_enabled']).to be(false)
+ expect(json_response['slack_app_id']).to be_nil
+ expect(json_response['slack_app_secret']).to be_nil
+ expect(json_response['slack_app_signing_secret']).to be_nil
+ expect(json_response['slack_app_verification_token']).to be_nil
+ expect(json_response['valid_runner_registrars']).to match_array(%w(project group))
+ expect(json_response['ci_max_includes']).to eq(150)
end
end
@@ -169,7 +179,16 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
jira_connect_proxy_url: 'http://example.com',
bulk_import_enabled: false,
allow_runner_registration_token: true,
- user_defaults_to_private_profile: true
+ user_defaults_to_private_profile: true,
+ default_syntax_highlighting_theme: 2,
+ projects_api_rate_limit_unauthenticated: 100,
+ silent_mode_enabled: true,
+ slack_app_enabled: true,
+ slack_app_id: 'SLACK_APP_ID',
+ slack_app_secret: 'SLACK_APP_SECRET',
+ slack_app_signing_secret: 'SLACK_APP_SIGNING_SECRET',
+ slack_app_verification_token: 'SLACK_APP_VERIFICATION_TOKEN',
+ valid_runner_registrars: ['group']
}
expect(response).to have_gitlab_http_status(:ok)
@@ -237,6 +256,15 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
expect(json_response['bulk_import_enabled']).to be(false)
expect(json_response['allow_runner_registration_token']).to be(true)
expect(json_response['user_defaults_to_private_profile']).to be(true)
+ expect(json_response['default_syntax_highlighting_theme']).to eq(2)
+ expect(json_response['projects_api_rate_limit_unauthenticated']).to be(100)
+ expect(json_response['silent_mode_enabled']).to be(true)
+ expect(json_response['slack_app_enabled']).to be(true)
+ expect(json_response['slack_app_id']).to eq('SLACK_APP_ID')
+ expect(json_response['slack_app_secret']).to eq('SLACK_APP_SECRET')
+ expect(json_response['slack_app_signing_secret']).to eq('SLACK_APP_SIGNING_SECRET')
+ expect(json_response['slack_app_verification_token']).to eq('SLACK_APP_VERIFICATION_TOKEN')
+ expect(json_response['valid_runner_registrars']).to eq(['group'])
end
end
@@ -807,6 +835,40 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
end
end
+ context 'with ci_max_includes' do
+ it 'updates the settings' do
+ put api("/application/settings", admin), params: {
+ ci_max_includes: 200
+ }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ 'ci_max_includes' => 200
+ )
+ end
+
+ it 'allows a zero value' do
+ put api("/application/settings", admin), params: {
+ ci_max_includes: 0
+ }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to include(
+ 'ci_max_includes' => 0
+ )
+ end
+
+ it 'does not allow a nil value' do
+ put api("/application/settings", admin), params: {
+ ci_max_includes: nil
+ }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']['ci_max_includes'])
+ .to include(a_string_matching('is not a number'))
+ end
+ end
+
context 'with housekeeping enabled' do
it 'at least one of housekeeping_incremental_repack_period or housekeeping_optimize_repository_period is required' do
put api("/application/settings", admin), params: {
diff --git a/spec/requests/api/sidekiq_metrics_spec.rb b/spec/requests/api/sidekiq_metrics_spec.rb
index 1085df97cc7..1ac065f0c0c 100644
--- a/spec/requests/api/sidekiq_metrics_spec.rb
+++ b/spec/requests/api/sidekiq_metrics_spec.rb
@@ -2,12 +2,19 @@
require 'spec_helper'
-RSpec.describe API::SidekiqMetrics, feature_category: :not_owned do
+RSpec.describe API::SidekiqMetrics, :aggregate_failures, feature_category: :shared do
let(:admin) { create(:user, :admin) }
describe 'GET sidekiq/*' do
+ %w[/sidekiq/queue_metrics /sidekiq/process_metrics /sidekiq/job_stats
+ /sidekiq/compound_metrics].each do |path|
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:path) { path }
+ end
+ end
+
it 'defines the `queue_metrics` endpoint' do
- get api('/sidekiq/queue_metrics', admin)
+ get api('/sidekiq/queue_metrics', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to match a_hash_including(
@@ -25,14 +32,14 @@ RSpec.describe API::SidekiqMetrics, feature_category: :not_owned do
end
it 'defines the `process_metrics` endpoint' do
- get api('/sidekiq/process_metrics', admin)
+ get api('/sidekiq/process_metrics', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['processes']).to be_an Array
end
it 'defines the `job_stats` endpoint' do
- get api('/sidekiq/job_stats', admin)
+ get api('/sidekiq/job_stats', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_a Hash
@@ -43,7 +50,7 @@ RSpec.describe API::SidekiqMetrics, feature_category: :not_owned do
end
it 'defines the `compound_metrics` endpoint' do
- get api('/sidekiq/compound_metrics', admin)
+ get api('/sidekiq/compound_metrics', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_a Hash
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index 2bc4c177bc9..4ba2a768e01 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Snippets, factory_default: :keep, feature_category: :source_code_management do
+RSpec.describe API::Snippets, :aggregate_failures, factory_default: :keep, feature_category: :source_code_management do
include SnippetHelpers
let_it_be(:admin) { create(:user, :admin) }
@@ -448,7 +448,7 @@ RSpec.describe API::Snippets, factory_default: :keep, feature_category: :source_
end
context "when admin" do
- let_it_be(:token) { create(:personal_access_token, user: admin, scopes: [:sudo]) }
+ let_it_be(:token) { create(:personal_access_token, :admin_mode, user: admin, scopes: [:sudo]) }
subject do
put api("/snippets/#{snippet.id}", personal_access_token: token), params: { visibility: 'private', sudo: user.id }
@@ -499,23 +499,19 @@ RSpec.describe API::Snippets, factory_default: :keep, feature_category: :source_
end
describe "GET /snippets/:id/user_agent_detail" do
- let(:snippet) { public_snippet }
+ let(:path) { "/snippets/#{public_snippet.id}/user_agent_detail" }
- it 'exposes known attributes' do
- user_agent_detail = create(:user_agent_detail, subject: snippet)
+ let_it_be(:user_agent_detail) { create(:user_agent_detail, subject: public_snippet) }
+
+ it_behaves_like 'GET request permissions for admin mode'
- get api("/snippets/#{snippet.id}/user_agent_detail", admin)
+ it 'exposes known attributes' do
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['user_agent']).to eq(user_agent_detail.user_agent)
expect(json_response['ip_address']).to eq(user_agent_detail.ip_address)
expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted)
end
-
- it "returns unauthorized for non-admin users" do
- get api("/snippets/#{snippet.id}/user_agent_detail", user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
end
end
diff --git a/spec/requests/api/statistics_spec.rb b/spec/requests/api/statistics_spec.rb
index 85fed48a077..baac39abf2c 100644
--- a/spec/requests/api/statistics_spec.rb
+++ b/spec/requests/api/statistics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Statistics, 'Statistics', feature_category: :devops_reports do
+RSpec.describe API::Statistics, 'Statistics', :aggregate_failures, feature_category: :devops_reports do
include ProjectForksHelper
tables_to_analyze = %w[
projects
@@ -21,6 +21,8 @@ RSpec.describe API::Statistics, 'Statistics', feature_category: :devops_reports
let(:path) { "/application/statistics" }
describe "GET /application/statistics" do
+ it_behaves_like 'GET request permissions for admin mode'
+
context 'when no user' do
it "returns authentication error" do
get api(path, nil)
@@ -43,7 +45,7 @@ RSpec.describe API::Statistics, 'Statistics', feature_category: :devops_reports
let(:admin) { create(:admin) }
it 'matches the response schema' do
- get api(path, admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('statistics')
@@ -66,7 +68,7 @@ RSpec.describe API::Statistics, 'Statistics', feature_category: :devops_reports
ApplicationRecord.connection.execute("ANALYZE #{table}")
end
- get api(path, admin)
+ get api(path, admin, admin_mode: true)
expected_statistics = {
issues: 2,
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index ab5e04246e8..604631bbf7f 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -178,7 +178,7 @@ RSpec.describe API::Tags, feature_category: :source_code_management do
end
end
- context 'with keyset pagination option', :aggregate_errors do
+ context 'with keyset pagination option', :aggregate_failures do
let(:base_params) { { pagination: 'keyset' } }
context 'with gitaly pagination params' do
diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb
index 2bd7cb027aa..f479ca25f3c 100644
--- a/spec/requests/api/terraform/modules/v1/packages_spec.rb
+++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb
@@ -415,12 +415,15 @@ RSpec.describe API::Terraform::Modules::V1::Packages, feature_category: :package
with_them do
let(:snowplow_gitlab_standard_context) do
- {
+ context = {
project: project,
- user: user_role == :anonymous ? nil : user,
namespace: project.namespace,
property: 'i_package_terraform_module_user'
}
+
+ context[:user] = user if user_role != :anonymous
+
+ context
end
before do
diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb
index fd34345d814..4c9f930df2f 100644
--- a/spec/requests/api/terraform/state_spec.rb
+++ b/spec/requests/api/terraform/state_spec.rb
@@ -21,6 +21,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
before do
stub_terraform_state_object_storage
+ stub_config(terraform_state: { enabled: true })
end
shared_examples 'endpoint with unique user tracking' do
@@ -51,7 +52,6 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
it_behaves_like 'Snowplow event tracking with RedisHLL context' do
subject(:api_request) { request }
- let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:category) { described_class.name }
let(:action) { 'terraform_state_api_request' }
let(:label) { 'redis_hll_counters.terraform.p_terraform_state_api_unique_users_monthly' }
@@ -82,6 +82,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
subject(:request) { get api(state_path), headers: auth_header }
it_behaves_like 'endpoint with unique user tracking'
+ it_behaves_like 'it depends on value of the `terraform_state.enabled` config'
context 'without authentication' do
let(:auth_header) { basic_auth_header('bad', 'token') }
@@ -113,17 +114,6 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
end
end
- context 'allow_dots_on_tf_state_names is disabled, and the state name contains a dot' do
- let(:state_name) { 'state-name-with-dot' }
- let(:state_path) { "/projects/#{project_id}/terraform/state/#{state_name}.tfstate" }
-
- before do
- stub_feature_flags(allow_dots_on_tf_state_names: false)
- end
-
- it_behaves_like 'can access terraform state'
- end
-
context 'for a project that does not exist' do
let(:project_id) { '0000' }
@@ -194,6 +184,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
subject(:request) { post api(state_path), headers: auth_header, as: :json, params: params }
it_behaves_like 'endpoint with unique user tracking'
+ it_behaves_like 'it depends on value of the `terraform_state.enabled` config'
context 'when terraform state with a given name is already present' do
context 'with maintainer permissions' do
@@ -275,21 +266,6 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
expect(Gitlab::Json.parse(response.body)).to be_empty
end
end
-
- context 'allow_dots_on_tf_state_names is disabled, and the state name contains a dot' do
- let(:non_existing_state_name) { 'state-name-with-dot.tfstate' }
-
- before do
- stub_feature_flags(allow_dots_on_tf_state_names: false)
- end
-
- it 'strips characters after the dot' do
- expect { request }.to change { Terraform::State.count }.by(1)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(Terraform::State.last.name).to eq('state-name-with-dot')
- end
- end
end
context 'without body' do
@@ -372,6 +348,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
subject(:request) { delete api(state_path), headers: auth_header }
it_behaves_like 'endpoint with unique user tracking'
+ it_behaves_like 'it depends on value of the `terraform_state.enabled` config'
shared_examples 'schedules the state for deletion' do
it 'returns empty body' do
@@ -396,18 +373,6 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
it_behaves_like 'schedules the state for deletion'
end
- context 'allow_dots_on_tf_state_names is disabled, and the state name contains a dot' do
- let(:state_name) { 'state-name-with-dot' }
- let(:state_name_with_dot) { "#{state_name}.tfstate" }
- let(:state_path) { "/projects/#{project_id}/terraform/state/#{state_name_with_dot}" }
-
- before do
- stub_feature_flags(allow_dots_on_tf_state_names: false)
- end
-
- it_behaves_like 'schedules the state for deletion'
- end
-
context 'with invalid state name' do
let(:state_name) { 'foo/bar' }
@@ -469,6 +434,7 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
request
expect(response).to have_gitlab_http_status(:conflict)
+ expect(Gitlab::Json.parse(response.body)).to include('Who' => current_user.username)
end
end
@@ -496,30 +462,10 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
context 'with a dot in the state name' do
let(:state_name) { 'test.state' }
- context 'with allow_dots_on_tf_state_names ff enabled' do
- before do
- stub_feature_flags(allow_dots_on_tf_state_names: true)
- end
-
- let(:state_name) { 'test.state' }
-
- it 'locks the terraform state' do
- request
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- context 'with allow_dots_on_tf_state_names ff disabled' do
- before do
- stub_feature_flags(allow_dots_on_tf_state_names: false)
- end
-
- it 'returns 404' do
- request
+ it 'locks the terraform state' do
+ request
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ expect(response).to have_gitlab_http_status(:ok)
end
end
end
@@ -540,7 +486,6 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
before do
state.lock_xid = '123.456'
state.save!
- stub_feature_flags(allow_dots_on_tf_state_names: true)
end
subject(:request) { delete api("#{state_path}/lock"), headers: auth_header, params: params }
@@ -553,6 +498,10 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
let(:lock_id) { 'irrelevant to this test, just needs to be present' }
end
+ it_behaves_like 'it depends on value of the `terraform_state.enabled` config' do
+ let(:lock_id) { '123.456' }
+ end
+
where(given_state_name: %w[test-state test.state test%2Ffoo])
with_them do
let(:state_name) { given_state_name }
@@ -567,23 +516,6 @@ RSpec.describe API::Terraform::State, :snowplow, feature_category: :infrastructu
end
end
- context 'with allow_dots_on_tf_state_names ff disabled' do
- before do
- stub_feature_flags(allow_dots_on_tf_state_names: false)
- end
-
- context 'with dots in the state name' do
- let(:lock_id) { '123.456' }
- let(:state_name) { 'test.state' }
-
- it 'returns 404' do
- request
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
context 'with no lock id (force-unlock)' do
let(:params) { {} }
diff --git a/spec/requests/api/terraform/state_version_spec.rb b/spec/requests/api/terraform/state_version_spec.rb
index 28abbb5749d..94fd2984435 100644
--- a/spec/requests/api/terraform/state_version_spec.rb
+++ b/spec/requests/api/terraform/state_version_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe API::Terraform::StateVersion, feature_category: :infrastructure_a
let_it_be(:maintainer) { create(:user, maintainer_projects: [project]) }
let_it_be(:user_without_access) { create(:user) }
- let_it_be(:state) { create(:terraform_state, project: project) }
+ let_it_be_with_reload(:state) { create(:terraform_state, project: project) }
let!(:versions) { create_list(:terraform_state_version, 3, terraform_state: state) }
@@ -22,9 +22,15 @@ RSpec.describe API::Terraform::StateVersion, feature_category: :infrastructure_a
let(:version_serial) { version.version }
let(:state_version_path) { "/projects/#{project_id}/terraform/state/#{state_name}/versions/#{version_serial}" }
+ before do
+ stub_config(terraform_state: { enabled: true })
+ end
+
describe 'GET /projects/:id/terraform/state/:name/versions/:serial' do
subject(:request) { get api(state_version_path), headers: auth_header }
+ it_behaves_like 'it depends on value of the `terraform_state.enabled` config'
+
context 'with invalid authentication' do
let(:auth_header) { basic_auth_header('bad', 'token') }
@@ -147,6 +153,8 @@ RSpec.describe API::Terraform::StateVersion, feature_category: :infrastructure_a
describe 'DELETE /projects/:id/terraform/state/:name/versions/:serial' do
subject(:request) { delete api(state_version_path), headers: auth_header }
+ it_behaves_like 'it depends on value of the `terraform_state.enabled` config', { success_status: :no_content }
+
context 'with invalid authentication' do
let(:auth_header) { basic_auth_header('bad', 'token') }
diff --git a/spec/requests/api/topics_spec.rb b/spec/requests/api/topics_spec.rb
index 14719292557..560f22c94be 100644
--- a/spec/requests/api/topics_spec.rb
+++ b/spec/requests/api/topics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Topics, feature_category: :projects do
+RSpec.describe API::Topics, :aggregate_failures, feature_category: :projects do
include WorkhorseHelpers
let_it_be(:file) { fixture_file_upload('spec/fixtures/dk.png') }
@@ -14,9 +14,11 @@ RSpec.describe API::Topics, feature_category: :projects do
let_it_be(:admin) { create(:user, :admin) }
let_it_be(:user) { create(:user) }
- describe 'GET /topics', :aggregate_failures do
+ let(:path) { '/topics' }
+
+ describe 'GET /topics' do
it 'returns topics ordered by total_projects_count' do
- get api('/topics')
+ get api(path)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -40,13 +42,13 @@ RSpec.describe API::Topics, feature_category: :projects do
let_it_be(:topic_4) { create(:topic, name: 'unassigned topic', total_projects_count: 0) }
it 'returns topics without assigned projects' do
- get api('/topics'), params: { without_projects: true }
+ get api(path), params: { without_projects: true }
expect(json_response.map { |t| t['id'] }).to contain_exactly(topic_4.id)
end
it 'returns topics without assigned projects' do
- get api('/topics'), params: { without_projects: false }
+ get api(path), params: { without_projects: false }
expect(json_response.map { |t| t['id'] }).to contain_exactly(topic_1.id, topic_2.id, topic_3.id, topic_4.id)
end
@@ -66,7 +68,7 @@ RSpec.describe API::Topics, feature_category: :projects do
with_them do
it 'returns filtered topics' do
- get api('/topics'), params: { search: search }
+ get api(path), params: { search: search }
expect(json_response.map { |t| t['name'] }).to eq(result)
end
@@ -97,7 +99,7 @@ RSpec.describe API::Topics, feature_category: :projects do
with_them do
it 'returns paginated topics' do
- get api('/topics'), params: params
+ get api(path), params: params
expect(json_response.map { |t| t['name'] }).to eq(result)
end
@@ -105,7 +107,7 @@ RSpec.describe API::Topics, feature_category: :projects do
end
end
- describe 'GET /topic/:id', :aggregate_failures do
+ describe 'GET /topic/:id' do
it 'returns topic' do
get api("/topics/#{topic_2.id}")
@@ -130,10 +132,14 @@ RSpec.describe API::Topics, feature_category: :projects do
end
end
- describe 'POST /topics', :aggregate_failures do
+ describe 'POST /topics' do
+ let(:params) { { name: 'my-topic', title: 'My Topic' } }
+
+ it_behaves_like 'POST request permissions for admin mode'
+
context 'as administrator' do
it 'creates a topic' do
- post api('/topics/', admin), params: { name: 'my-topic', title: 'My Topic' }
+ post api('/topics/', admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq('my-topic')
@@ -142,7 +148,7 @@ RSpec.describe API::Topics, feature_category: :projects do
it 'creates a topic with avatar and description' do
workhorse_form_with_file(
- api('/topics/', admin),
+ api('/topics/', admin, admin_mode: true),
file_key: :avatar,
params: { name: 'my-topic', title: 'My Topic', description: 'my description...', avatar: file }
)
@@ -160,14 +166,14 @@ RSpec.describe API::Topics, feature_category: :projects do
end
it 'returns 400 if name is not unique (case insensitive)' do
- post api('/topics/', admin), params: { name: topic_1.name.downcase, title: 'My Topic' }
+ post api('/topics/', admin, admin_mode: true), params: { name: topic_1.name.downcase, title: 'My Topic' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']['name']).to eq(['has already been taken'])
end
it 'returns 400 if title is missing' do
- post api('/topics/', admin), params: { name: 'my-topic' }
+ post api('/topics/', admin, admin_mode: true), params: { name: 'my-topic' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eql('title is missing')
@@ -176,7 +182,7 @@ RSpec.describe API::Topics, feature_category: :projects do
context 'as normal user' do
it 'returns 403 Forbidden' do
- post api('/topics/', user), params: { name: 'my-topic', title: 'My Topic' }
+ post api('/topics/', user), params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -184,17 +190,23 @@ RSpec.describe API::Topics, feature_category: :projects do
context 'as anonymous' do
it 'returns 401 Unauthorized' do
- post api('/topics/'), params: { name: 'my-topic', title: 'My Topic' }
+ post api('/topics/'), params: params
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
- describe 'PUT /topics', :aggregate_failures do
+ describe 'PUT /topics' do
+ let(:params) { { name: 'my-topic' } }
+
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:path) { "/topics/#{topic_3.id}" }
+ end
+
context 'as administrator' do
it 'updates a topic' do
- put api("/topics/#{topic_3.id}", admin), params: { name: 'my-topic' }
+ put api("/topics/#{topic_3.id}", admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq('my-topic')
@@ -203,7 +215,7 @@ RSpec.describe API::Topics, feature_category: :projects do
it 'updates a topic with avatar and description' do
workhorse_form_with_file(
- api("/topics/#{topic_3.id}", admin),
+ api("/topics/#{topic_3.id}", admin, admin_mode: true),
method: :put,
file_key: :avatar,
params: { description: 'my description...', avatar: file }
@@ -215,7 +227,7 @@ RSpec.describe API::Topics, feature_category: :projects do
end
it 'keeps avatar when updating other fields' do
- put api("/topics/#{topic_1.id}", admin), params: { name: 'my-topic' }
+ put api("/topics/#{topic_1.id}", admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq('my-topic')
@@ -223,13 +235,13 @@ RSpec.describe API::Topics, feature_category: :projects do
end
it 'returns 404 for non existing id' do
- put api("/topics/#{non_existing_record_id}", admin), params: { name: 'my-topic' }
+ put api("/topics/#{non_existing_record_id}", admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns 400 for invalid `id` parameter' do
- put api('/topics/invalid', admin), params: { name: 'my-topic' }
+ put api('/topics/invalid', admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eql('id is invalid')
@@ -237,7 +249,7 @@ RSpec.describe API::Topics, feature_category: :projects do
context 'with blank avatar' do
it 'removes avatar' do
- put api("/topics/#{topic_1.id}", admin), params: { avatar: '' }
+ put api("/topics/#{topic_1.id}", admin, admin_mode: true), params: { avatar: '' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['avatar_url']).to be_nil
@@ -245,7 +257,7 @@ RSpec.describe API::Topics, feature_category: :projects do
end
it 'removes avatar besides other changes' do
- put api("/topics/#{topic_1.id}", admin), params: { name: 'new-topic-name', avatar: '' }
+ put api("/topics/#{topic_1.id}", admin, admin_mode: true), params: { name: 'new-topic-name', avatar: '' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq('new-topic-name')
@@ -254,7 +266,7 @@ RSpec.describe API::Topics, feature_category: :projects do
end
it 'does not remove avatar in case of other errors' do
- put api("/topics/#{topic_1.id}", admin), params: { name: topic_2.name, avatar: '' }
+ put api("/topics/#{topic_1.id}", admin, admin_mode: true), params: { name: topic_2.name, avatar: '' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(topic_1.reload.avatar_url).not_to be_nil
@@ -264,7 +276,7 @@ RSpec.describe API::Topics, feature_category: :projects do
context 'as normal user' do
it 'returns 403 Forbidden' do
- put api("/topics/#{topic_3.id}", user), params: { name: 'my-topic' }
+ put api("/topics/#{topic_3.id}", user), params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -272,29 +284,37 @@ RSpec.describe API::Topics, feature_category: :projects do
context 'as anonymous' do
it 'returns 401 Unauthorized' do
- put api("/topics/#{topic_3.id}"), params: { name: 'my-topic' }
+ put api("/topics/#{topic_3.id}"), params: params
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
- describe 'DELETE /topics', :aggregate_failures do
+ describe 'DELETE /topics/:id' do
+ let(:params) { { name: 'my-topic' } }
+
context 'as administrator' do
- it 'deletes a topic' do
- delete api("/topics/#{topic_3.id}", admin), params: { name: 'my-topic' }
+ it 'deletes a topic with admin mode' do
+ delete api("/topics/#{topic_3.id}", admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:no_content)
end
+ it 'deletes a topic without admin mode' do
+ delete api("/topics/#{topic_3.id}", admin, admin_mode: false), params: params
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
it 'returns 404 for non existing id' do
- delete api("/topics/#{non_existing_record_id}", admin), params: { name: 'my-topic' }
+ delete api("/topics/#{non_existing_record_id}", admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns 400 for invalid `id` parameter' do
- delete api('/topics/invalid', admin), params: { name: 'my-topic' }
+ delete api('/topics/invalid', admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eql('id is invalid')
@@ -303,7 +323,7 @@ RSpec.describe API::Topics, feature_category: :projects do
context 'as normal user' do
it 'returns 403 Forbidden' do
- delete api("/topics/#{topic_3.id}", user), params: { name: 'my-topic' }
+ delete api("/topics/#{topic_3.id}", user), params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -311,16 +331,21 @@ RSpec.describe API::Topics, feature_category: :projects do
context 'as anonymous' do
it 'returns 401 Unauthorized' do
- delete api("/topics/#{topic_3.id}"), params: { name: 'my-topic' }
+ delete api("/topics/#{topic_3.id}"), params: params
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
- describe 'POST /topics/merge', :aggregate_failures do
+ describe 'POST /topics/merge' do
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:path) { '/topics/merge' }
+ let(:params) { { source_topic_id: topic_3.id, target_topic_id: topic_2.id } }
+ end
+
context 'as administrator' do
- let_it_be(:api_url) { api('/topics/merge', admin) }
+ let_it_be(:api_url) { api('/topics/merge', admin, admin_mode: true) }
it 'merge topics' do
post api_url, params: { source_topic_id: topic_3.id, target_topic_id: topic_2.id }
diff --git a/spec/requests/api/unleash_spec.rb b/spec/requests/api/unleash_spec.rb
index 5daf7cd7b75..75b26b98228 100644
--- a/spec/requests/api/unleash_spec.rb
+++ b/spec/requests/api/unleash_spec.rb
@@ -88,6 +88,14 @@ RSpec.describe API::Unleash, feature_category: :feature_flags do
end
end
+ describe 'GET /feature_flags/unleash/:project_id/client/features', :use_clean_rails_redis_caching do
+ specify do
+ get api("/feature_flags/unleash/#{project_id}/client/features"), params: params, headers: headers
+
+ is_expected.to have_request_urgency(:medium)
+ end
+ end
+
%w(/feature_flags/unleash/:project_id/features /feature_flags/unleash/:project_id/client/features).each do |features_endpoint|
describe "GET #{features_endpoint}", :use_clean_rails_redis_caching do
let(:features_url) { features_endpoint.sub(':project_id', project_id.to_s) }
diff --git a/spec/requests/api/usage_data_non_sql_metrics_spec.rb b/spec/requests/api/usage_data_non_sql_metrics_spec.rb
index 0a6f248af2c..b2929caf676 100644
--- a/spec/requests/api/usage_data_non_sql_metrics_spec.rb
+++ b/spec/requests/api/usage_data_non_sql_metrics_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::UsageDataNonSqlMetrics, feature_category: :service_ping do
+RSpec.describe API::UsageDataNonSqlMetrics, :aggregate_failures, feature_category: :service_ping do
include UsageDataHelpers
let_it_be(:admin) { create(:user, admin: true) }
@@ -21,8 +21,12 @@ RSpec.describe API::UsageDataNonSqlMetrics, feature_category: :service_ping do
stub_database_flavor_check
end
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:path) { endpoint }
+ end
+
it 'returns non sql metrics if user is admin' do
- get api(endpoint, admin)
+ get api(endpoint, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['counts']).to be_a(Hash)
@@ -53,7 +57,7 @@ RSpec.describe API::UsageDataNonSqlMetrics, feature_category: :service_ping do
end
it 'returns not_found for admin' do
- get api(endpoint, admin)
+ get api(endpoint, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/requests/api/usage_data_queries_spec.rb b/spec/requests/api/usage_data_queries_spec.rb
index e556064025c..ab3c38adb81 100644
--- a/spec/requests/api/usage_data_queries_spec.rb
+++ b/spec/requests/api/usage_data_queries_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require 'rake_helper'
-RSpec.describe API::UsageDataQueries, feature_category: :service_ping do
+RSpec.describe API::UsageDataQueries, :aggregate_failures, feature_category: :service_ping do
include UsageDataHelpers
let_it_be(:admin) { create(:user, admin: true) }
@@ -22,8 +22,12 @@ RSpec.describe API::UsageDataQueries, feature_category: :service_ping do
stub_feature_flags(usage_data_queries_api: true)
end
+ it_behaves_like 'GET request permissions for admin mode' do
+ let(:path) { endpoint }
+ end
+
it 'returns queries if user is admin' do
- get api(endpoint, admin)
+ get api(endpoint, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['active_user_count']).to start_with('SELECT COUNT("users"."id") FROM "users"')
@@ -54,7 +58,7 @@ RSpec.describe API::UsageDataQueries, feature_category: :service_ping do
end
it 'returns not_found for admin' do
- get api(endpoint, admin)
+ get api(endpoint, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -81,7 +85,7 @@ RSpec.describe API::UsageDataQueries, feature_category: :service_ping do
it 'matches the generated query' do
travel_to(Time.utc(2021, 1, 1)) do
- get api(endpoint, admin)
+ get api(endpoint, admin, admin_mode: true)
end
data = Gitlab::Json.parse(File.read(file))
diff --git a/spec/requests/api/users_preferences_spec.rb b/spec/requests/api/users_preferences_spec.rb
index ef9735fd8b0..067acd150f3 100644
--- a/spec/requests/api/users_preferences_spec.rb
+++ b/spec/requests/api/users_preferences_spec.rb
@@ -10,17 +10,20 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns a success status and the value has been changed' do
put api("/user/preferences", user), params: {
view_diffs_file_by_file: true,
- show_whitespace_in_diffs: true
+ show_whitespace_in_diffs: true,
+ pass_user_identities_to_ci_jwt: true
}
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['view_diffs_file_by_file']).to eq(true)
expect(json_response['show_whitespace_in_diffs']).to eq(true)
+ expect(json_response['pass_user_identities_to_ci_jwt']).to eq(true)
user.reload
expect(user.view_diffs_file_by_file).to be_truthy
expect(user.show_whitespace_in_diffs).to be_truthy
+ expect(user.pass_user_identities_to_ci_jwt).to be_truthy
end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 34867b13db2..cc8be312c71 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::Users, feature_category: :user_profile do
+RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile do
include WorkhorseHelpers
let_it_be(:admin) { create(:admin) }
@@ -27,9 +27,15 @@ RSpec.describe API::Users, feature_category: :user_profile do
let_it_be(:user, reload: true) { create(:user, note: '2018-11-05 | 2FA removed | user requested | www.gitlab.com') }
describe 'POST /users' do
+ let(:path) { '/users' }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { attributes_for(:user).merge({ note: 'Awesome Note' }) }
+ end
+
context 'when unauthenticated' do
it 'return authentication error' do
- post api('/users')
+ post api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -41,7 +47,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
optional_attributes = { note: 'Awesome Note' }
attributes = attributes_for(:user).merge(optional_attributes)
- post api('/users', admin), params: attributes
+ post api(path, admin, admin_mode: true), params: attributes
expect(response).to have_gitlab_http_status(:created)
expect(json_response['note']).to eq(optional_attributes[:note])
@@ -50,7 +56,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'as a regular user' do
it 'does not allow creating new user' do
- post api('/users', user), params: attributes_for(:user)
+ post api(path, user), params: attributes_for(:user)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -59,12 +65,18 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe "PUT /users/:id" do
+ let(:path) { "/users/#{user.id}" }
+
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:params) { { note: 'new note' } }
+ end
+
context 'when user is an admin' do
it "updates note of the user" do
new_note = '2019-07-07 | Email changed | user requested | www.gitlab.com'
expect do
- put api("/users/#{user.id}", admin), params: { note: new_note }
+ put api(path, admin, admin_mode: true), params: { note: new_note }
end.to change { user.reload.note }
.from('2018-11-05 | 2FA removed | user requested | www.gitlab.com')
.to(new_note)
@@ -77,7 +89,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'when user is not an admin' do
it "cannot update their own note" do
expect do
- put api("/users/#{user.id}", user), params: { note: 'new note' }
+ put api(path, user), params: { note: 'new note' }
end.not_to change { user.reload.note }
expect(response).to have_gitlab_http_status(:forbidden)
@@ -89,7 +101,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context "when current user is an admin" do
it "returns a 204 when 2FA is disabled for the target user" do
expect do
- patch api("/users/#{user_with_2fa.id}/disable_two_factor", admin)
+ patch api("/users/#{user_with_2fa.id}/disable_two_factor", admin, admin_mode: true)
end.to change { user_with_2fa.reload.two_factor_enabled? }
.from(true)
.to(false)
@@ -103,14 +115,14 @@ RSpec.describe API::Users, feature_category: :user_profile do
.and_return(destroy_service)
expect(destroy_service).to receive(:execute)
- patch api("/users/#{user_with_2fa.id}/disable_two_factor", admin)
+ patch api("/users/#{user_with_2fa.id}/disable_two_factor", admin, admin_mode: true)
end
it "returns a 400 if 2FA is not enabled for the target user" do
expect(TwoFactor::DestroyService).to receive(:new).and_call_original
expect do
- patch api("/users/#{user.id}/disable_two_factor", admin)
+ patch api("/users/#{user.id}/disable_two_factor", admin, admin_mode: true)
end.not_to change { user.reload.two_factor_enabled? }
expect(response).to have_gitlab_http_status(:bad_request)
@@ -121,7 +133,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
expect(TwoFactor::DestroyService).not_to receive(:new)
expect do
- patch api("/users/#{admin_with_2fa.id}/disable_two_factor", admin)
+ patch api("/users/#{admin_with_2fa.id}/disable_two_factor", admin, admin_mode: true)
end.not_to change { admin_with_2fa.reload.two_factor_enabled? }
expect(response).to have_gitlab_http_status(:forbidden)
@@ -131,7 +143,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it "returns a 404 if the target user cannot be found" do
expect(TwoFactor::DestroyService).not_to receive(:new)
- patch api("/users/#{non_existing_record_id}/disable_two_factor", admin)
+ patch api("/users/#{non_existing_record_id}/disable_two_factor", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq("404 User Not Found")
@@ -159,9 +171,11 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe 'GET /users/' do
+ let(:path) { '/users' }
+
context 'when unauthenticated' do
it "does not contain certain fields" do
- get api("/users"), params: { username: user.username }
+ get api(path), params: { username: user.username }
expect(json_response.first).not_to have_key('note')
expect(json_response.first).not_to have_key('namespace_id')
@@ -172,8 +186,9 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'when authenticated' do
context 'as a regular user' do
it 'does not contain certain fields' do
- get api("/users", user), params: { username: user.username }
+ get api(path, user), params: { username: user.username }
+ expect(response).to have_gitlab_http_status(:ok)
expect(json_response.first).not_to have_key('note')
expect(json_response.first).not_to have_key('namespace_id')
expect(json_response.first).not_to have_key('created_by')
@@ -182,7 +197,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'as an admin' do
it 'contains the note of users' do
- get api("/users", admin), params: { username: user.username }
+ get api(path, admin, admin_mode: true), params: { username: user.username }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.first).to have_key('note')
@@ -191,7 +206,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'with `created_by` details' do
it 'has created_by as nil with a self-registered account' do
- get api("/users", admin), params: { username: user.username }
+ get api(path, admin, admin_mode: true), params: { username: user.username }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.first).to have_key('created_by')
@@ -201,7 +216,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'is created_by a user and has those details' do
created = create(:user, created_by_id: user.id)
- get api("/users", admin), params: { username: created.username }
+ get api(path, admin, admin_mode: true), params: { username: created.username }
expect(response).to have_gitlab_http_status(:success)
expect(json_response.first['created_by'].symbolize_keys)
@@ -217,7 +232,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'avoids N+1 queries when requested by admin' do
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api("/users", admin)
+ get api(path, admin)
end.count
create_list(:user, 3)
@@ -227,19 +242,19 @@ RSpec.describe API::Users, feature_category: :user_profile do
# Refer issue https://gitlab.com/gitlab-org/gitlab/-/issues/367080
expect do
- get api("/users", admin)
+ get api(path, admin)
end.not_to exceed_all_query_limit(control_count + 3)
end
it 'avoids N+1 queries when requested by a regular user' do
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api("/users", user)
+ get api(path, user)
end.count
create_list(:user, 3)
expect do
- get api("/users", user)
+ get api(path, user)
end.not_to exceed_all_query_limit(control_count)
end
end
@@ -247,11 +262,13 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe 'GET /user' do
+ let(:path) { '/user' }
+
context 'when authenticated' do
context 'as an admin' do
context 'accesses their own profile' do
it 'contains the note of the user' do
- get api("/user", admin)
+ get api(path, admin, admin_mode: true)
expect(json_response).to have_key('note')
expect(json_response['note']).to eq(admin.note)
@@ -259,7 +276,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
context 'sudo' do
- let(:admin_personal_access_token) { create(:personal_access_token, user: admin, scopes: %w[api sudo]).token }
+ let(:admin_personal_access_token) { create(:personal_access_token, :admin_mode, user: admin, scopes: %w[api sudo]).token }
context 'accesses the profile of another regular user' do
it 'does not contain the note of the user' do
@@ -286,7 +303,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'as a regular user' do
it 'does not contain the note of the user' do
- get api("/user", user)
+ get api(path, user)
expect(json_response).not_to have_key('note')
expect(json_response).not_to have_key('namespace_id')
@@ -318,15 +335,17 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe 'GET /users' do
+ let(:path) { '/users' }
+
context "when unauthenticated" do
it "returns authorization error when the `username` parameter is not passed" do
- get api("/users")
+ get api(path)
expect(response).to have_gitlab_http_status(:forbidden)
end
it "returns the user when a valid `username` parameter is passed" do
- get api("/users"), params: { username: user.username }
+ get api(path), params: { username: user.username }
expect(response).to match_response_schema('public_api/v4/user/basics')
expect(json_response.size).to eq(1)
@@ -335,7 +354,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "returns the user when a valid `username` parameter is passed (case insensitive)" do
- get api("/users"), params: { username: user.username.upcase }
+ get api(path), params: { username: user.username.upcase }
expect(response).to match_response_schema('public_api/v4/user/basics')
expect(json_response.size).to eq(1)
@@ -344,7 +363,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "returns an empty response when an invalid `username` parameter is passed" do
- get api("/users"), params: { username: 'invalid' }
+ get api(path), params: { username: 'invalid' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
@@ -352,14 +371,14 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "does not return the highest role" do
- get api("/users"), params: { username: user.username }
+ get api(path), params: { username: user.username }
expect(response).to match_response_schema('public_api/v4/user/basics')
expect(json_response.first.keys).not_to include 'highest_role'
end
it "does not return the current or last sign-in ip addresses" do
- get api("/users"), params: { username: user.username }
+ get api(path), params: { username: user.username }
expect(response).to match_response_schema('public_api/v4/user/basics')
expect(json_response.first.keys).not_to include 'current_sign_in_ip'
@@ -372,13 +391,13 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "returns authorization error when the `username` parameter refers to an inaccessible user" do
- get api("/users"), params: { username: user.username }
+ get api(path), params: { username: user.username }
expect(response).to have_gitlab_http_status(:forbidden)
end
it "returns authorization error when the `username` parameter is not passed" do
- get api("/users")
+ get api(path)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -394,7 +413,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'when authenticate as a regular user' do
it "renders 200" do
- get api("/users", user)
+ get api(path, user)
expect(response).to match_response_schema('public_api/v4/user/basics')
end
@@ -402,7 +421,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'when authenticate as an admin' do
it "renders 200" do
- get api("/users", admin)
+ get api(path, admin)
expect(response).to match_response_schema('public_api/v4/user/basics')
end
@@ -410,7 +429,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "returns an array of users" do
- get api("/users", user)
+ get api(path, user)
expect(response).to match_response_schema('public_api/v4/user/basics')
expect(response).to include_pagination_headers
@@ -466,7 +485,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'does not reveal the `is_admin` flag of the user' do
- get api('/users', user)
+ get api(path, user)
expect(response).to match_response_schema('public_api/v4/user/basics')
expect(json_response.first.keys).not_to include 'is_admin'
@@ -528,7 +547,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context "when admin" do
context 'when sudo is defined' do
it 'does not return 500' do
- admin_personal_access_token = create(:personal_access_token, user: admin, scopes: [:sudo])
+ admin_personal_access_token = create(:personal_access_token, :admin_mode, user: admin, scopes: [:sudo])
get api("/users?sudo=#{user.id}", admin, personal_access_token: admin_personal_access_token)
expect(response).to have_gitlab_http_status(:success)
@@ -536,14 +555,14 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "returns an array of users" do
- get api("/users", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to match_response_schema('public_api/v4/user/admins')
expect(response).to include_pagination_headers
end
it "users contain the `namespace_id` field" do
- get api("/users", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:success)
expect(response).to match_response_schema('public_api/v4/user/admins')
@@ -554,7 +573,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it "returns an array of external users" do
create(:user, external: true)
- get api("/users?external=true", admin)
+ get api("/users?external=true", admin, admin_mode: true)
expect(response).to match_response_schema('public_api/v4/user/admins')
expect(response).to include_pagination_headers
@@ -562,7 +581,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "returns one user by external UID" do
- get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}&provider=#{omniauth_user.identities.first.provider}", admin)
+ get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}&provider=#{omniauth_user.identities.first.provider}", admin, admin_mode: true)
expect(response).to match_response_schema('public_api/v4/user/admins')
expect(json_response.size).to eq(1)
@@ -570,13 +589,13 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "returns 400 error if provider with no extern_uid" do
- get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}", admin)
+ get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
it "returns 400 error if provider with no extern_uid" do
- get api("/users?provider=#{omniauth_user.identities.first.provider}", admin)
+ get api("/users?provider=#{omniauth_user.identities.first.provider}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -584,7 +603,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it "returns a user created before a specific date" do
user = create(:user, created_at: Date.new(2000, 1, 1))
- get api("/users?created_before=2000-01-02T00:00:00.060Z", admin)
+ get api("/users?created_before=2000-01-02T00:00:00.060Z", admin, admin_mode: true)
expect(response).to match_response_schema('public_api/v4/user/admins')
expect(json_response.size).to eq(1)
@@ -594,7 +613,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it "returns no users created before a specific date" do
create(:user, created_at: Date.new(2001, 1, 1))
- get api("/users?created_before=2000-01-02T00:00:00.060Z", admin)
+ get api("/users?created_before=2000-01-02T00:00:00.060Z", admin, admin_mode: true)
expect(response).to match_response_schema('public_api/v4/user/admins')
expect(json_response.size).to eq(0)
@@ -603,7 +622,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it "returns users created before and after a specific date" do
user = create(:user, created_at: Date.new(2001, 1, 1))
- get api("/users?created_before=2001-01-02T00:00:00.060Z&created_after=1999-01-02T00:00:00.060", admin)
+ get api("/users?created_before=2001-01-02T00:00:00.060Z&created_after=1999-01-02T00:00:00.060", admin, admin_mode: true)
expect(response).to match_response_schema('public_api/v4/user/admins')
expect(json_response.size).to eq(1)
@@ -615,7 +634,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
# - admin
# - user
- get api('/users', admin), params: { order_by: 'id', sort: 'asc' }
+ get api(path, admin, admin_mode: true), params: { order_by: 'id', sort: 'asc' }
expect(response).to match_response_schema('public_api/v4/user/admins')
expect(json_response.size).to eq(2)
@@ -626,7 +645,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns users with 2fa enabled' do
user_with_2fa = create(:user, :two_factor_via_otp)
- get api('/users', admin), params: { two_factor: 'enabled' }
+ get api(path, admin, admin_mode: true), params: { two_factor: 'enabled' }
expect(response).to match_response_schema('public_api/v4/user/admins')
expect(json_response.size).to eq(1)
@@ -638,7 +657,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
create(:project, namespace: user.namespace)
create(:project, namespace: admin.namespace)
- get api('/users', admin), params: { without_projects: true }
+ get api(path, admin, admin_mode: true), params: { without_projects: true }
expect(response).to match_response_schema('public_api/v4/user/admins')
expect(json_response.size).to eq(1)
@@ -646,7 +665,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'returns 400 when provided incorrect sort params' do
- get api('/users', admin), params: { order_by: 'magic', sort: 'asc' }
+ get api(path, admin, admin_mode: true), params: { order_by: 'magic', sort: 'asc' }
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -654,7 +673,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'admins param' do
it 'returns only admins' do
- get api("/users?admins=true", admin)
+ get api("/users?admins=true", admin, admin_mode: true)
expect(response).to match_response_schema('public_api/v4/user/basics')
expect(json_response.size).to eq(1)
@@ -666,34 +685,36 @@ RSpec.describe API::Users, feature_category: :user_profile do
describe "GET /users/:id" do
let_it_be(:user2, reload: true) { create(:user, username: 'another_user') }
+ let(:path) { "/users/#{user.id}" }
+
before do
allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?)
.with(:users_get_by_id, scope: user, users_allowlist: []).and_return(false)
end
it "returns a user by id" do
- get api("/users/#{user.id}", user)
+ get api(path, user)
expect(response).to match_response_schema('public_api/v4/user/basic')
expect(json_response['username']).to eq(user.username)
end
it "does not return the user's `is_admin` flag" do
- get api("/users/#{user.id}", user)
+ get api(path, user)
expect(response).to match_response_schema('public_api/v4/user/basic')
expect(json_response.keys).not_to include 'is_admin'
end
it "does not return the user's `highest_role`" do
- get api("/users/#{user.id}", user)
+ get api(path, user)
expect(response).to match_response_schema('public_api/v4/user/basic')
expect(json_response.keys).not_to include 'highest_role'
end
it "does not return the user's sign in IPs" do
- get api("/users/#{user.id}", user)
+ get api(path, user)
expect(response).to match_response_schema('public_api/v4/user/basic')
expect(json_response.keys).not_to include 'current_sign_in_ip'
@@ -701,7 +722,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "does not contain plan or trial data" do
- get api("/users/#{user.id}", user)
+ get api(path, user)
expect(response).to match_response_schema('public_api/v4/user/basic')
expect(json_response.keys).not_to include 'plan'
@@ -760,7 +781,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'does not contain the note of the user' do
- get api("/users/#{user.id}", user)
+ get api(path, user)
expect(json_response).not_to have_key('note')
expect(json_response).not_to have_key('sign_in_count')
@@ -772,7 +793,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
.to receive(:throttled?).with(:users_get_by_id, scope: user, users_allowlist: [])
.and_return(false)
- get api("/users/#{user.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
end
@@ -785,7 +806,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
.to receive(:throttled?).with(:users_get_by_id, scope: user, users_allowlist: [])
.and_return(true)
- get api("/users/#{user.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:too_many_requests)
end
@@ -794,7 +815,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
expect(Gitlab::ApplicationRateLimiter)
.not_to receive(:throttled?)
- get api("/users/#{user.id}", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
end
@@ -812,7 +833,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
.to receive(:throttled?).with(:users_get_by_id, scope: user, users_allowlist: allowlist)
.and_call_original
- get api("/users/#{user.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
end
@@ -827,7 +848,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'returns job title of a user' do
- get api("/users/#{user.id}", user)
+ get api(path, user)
expect(response).to match_response_schema('public_api/v4/user/basic')
expect(json_response['job_title']).to eq(job_title)
@@ -836,7 +857,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'when authenticated as admin' do
it 'contains the note of the user' do
- get api("/users/#{user.id}", admin)
+ get api(path, admin, admin_mode: true)
expect(json_response).to have_key('note')
expect(json_response['note']).to eq(user.note)
@@ -844,28 +865,28 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'includes the `is_admin` field' do
- get api("/users/#{user.id}", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to match_response_schema('public_api/v4/user/admin')
expect(json_response['is_admin']).to be(false)
end
it "includes the `created_at` field for private users" do
- get api("/users/#{private_user.id}", admin)
+ get api("/users/#{private_user.id}", admin, admin_mode: true)
expect(response).to match_response_schema('public_api/v4/user/admin')
expect(json_response.keys).to include 'created_at'
end
it 'includes the `highest_role` field' do
- get api("/users/#{user.id}", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to match_response_schema('public_api/v4/user/admin')
expect(json_response['highest_role']).to be(0)
end
it 'includes the `namespace_id` field' do
- get api("/users/#{user.id}", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:success)
expect(response).to match_response_schema('public_api/v4/user/admin')
@@ -874,13 +895,13 @@ RSpec.describe API::Users, feature_category: :user_profile do
if Gitlab.ee?
it 'does not include values for plan or trial' do
- get api("/users/#{user.id}", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to match_response_schema('public_api/v4/user/basic')
end
else
it 'does not include plan or trial data' do
- get api("/users/#{user.id}", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to match_response_schema('public_api/v4/user/basic')
expect(json_response.keys).not_to include 'plan'
@@ -890,7 +911,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'when user has not logged in' do
it 'does not include the sign in IPs' do
- get api("/users/#{user.id}", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to match_response_schema('public_api/v4/user/admin')
expect(json_response).to include('current_sign_in_ip' => nil, 'last_sign_in_ip' => nil)
@@ -901,7 +922,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
let_it_be(:signed_in_user) { create(:user, :with_sign_ins) }
it 'includes the sign in IPs' do
- get api("/users/#{signed_in_user.id}", admin)
+ get api("/users/#{signed_in_user.id}", admin, admin_mode: true)
expect(response).to match_response_schema('public_api/v4/user/admin')
expect(json_response['current_sign_in_ip']).to eq('127.0.0.1')
@@ -912,14 +933,14 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'for an anonymous user' do
it 'returns 403' do
- get api("/users/#{user.id}")
+ get api(path)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
it "returns a 404 error if user id not found" do
- get api("/users/0", user)
+ get api("/users/#{non_existing_record_id}", user)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
@@ -954,10 +975,11 @@ RSpec.describe API::Users, feature_category: :user_profile do
describe 'POST /users/:id/follow' do
let(:followee) { create(:user) }
+ let(:path) { "/users/#{followee.id}/follow" }
context 'on an unfollowed user' do
it 'follows the user' do
- post api("/users/#{followee.id}/follow", user)
+ post api(path, user)
expect(user.followees).to contain_exactly(followee)
expect(response).to have_gitlab_http_status(:created)
@@ -967,7 +989,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
stub_const('Users::UserFollowUser::MAX_FOLLOWEE_LIMIT', 2)
Users::UserFollowUser::MAX_FOLLOWEE_LIMIT.times { user.follow(create(:user)) }
- post api("/users/#{followee.id}/follow", user)
+ post api(path, user)
expect(response).to have_gitlab_http_status(:bad_request)
expected_message = format(_("You can't follow more than %{limit} users. To follow more users, unfollow some others."), limit: Users::UserFollowUser::MAX_FOLLOWEE_LIMIT)
expect(json_response['message']).to eq(expected_message)
@@ -981,16 +1003,31 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'does not change following' do
- post api("/users/#{followee.id}/follow", user)
+ post api(path, user)
expect(user.followees).to contain_exactly(followee)
expect(response).to have_gitlab_http_status(:not_modified)
end
end
+
+ context 'on a user with disabled following' do
+ before do
+ user.enabled_following = false
+ user.save!
+ end
+
+ it 'does not change following' do
+ post api("/users/#{followee.id}/follow", user)
+
+ expect(user.followees).to be_empty
+ expect(response).to have_gitlab_http_status(:not_modified)
+ end
+ end
end
describe 'POST /users/:id/unfollow' do
let(:followee) { create(:user) }
+ let(:path) { "/users/#{followee.id}/unfollow" }
context 'on a followed user' do
before do
@@ -998,7 +1035,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'unfollow the user' do
- post api("/users/#{followee.id}/unfollow", user)
+ post api(path, user)
expect(user.followees).to be_empty
expect(response).to have_gitlab_http_status(:created)
@@ -1007,7 +1044,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'on an unfollowed user' do
it 'does not change following' do
- post api("/users/#{followee.id}/unfollow", user)
+ post api(path, user)
expect(user.followees).to be_empty
expect(response).to have_gitlab_http_status(:not_modified)
@@ -1017,6 +1054,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
describe 'GET /users/:id/followers' do
let(:follower) { create(:user) }
+ let(:path) { "/users/#{user.id}/followers" }
context 'for an anonymous user' do
it 'returns 403' do
@@ -1030,7 +1068,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'lists followers' do
follower.follow(user)
- get api("/users/#{user.id}/followers", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -1049,7 +1087,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'user does not have any follower' do
it 'does list nothing' do
- get api("/users/#{user.id}/followers", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -1060,6 +1098,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
describe 'GET /users/:id/following' do
let(:followee) { create(:user) }
+ let(:path) { "/users/#{user.id}/followers" }
context 'for an anonymous user' do
it 'returns 403' do
@@ -1073,7 +1112,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'lists following user' do
user.follow(followee)
- get api("/users/#{user.id}/following", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -1092,7 +1131,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'user does not have any follower' do
it 'does list nothing' do
- get api("/users/#{user.id}/following", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -1102,14 +1141,20 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe "POST /users" do
+ let(:path) { '/users' }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { attributes_for(:user, projects_limit: 3) }
+ end
+
it "creates user" do
expect do
- post api("/users", admin), params: attributes_for(:user, projects_limit: 3)
+ post api(path, admin, admin_mode: true), params: attributes_for(:user, projects_limit: 3)
end.to change { User.count }.by(1)
end
it "creates user with correct attributes" do
- post api('/users', admin), params: attributes_for(:user, admin: true, can_create_group: true)
+ post api(path, admin, admin_mode: true), params: attributes_for(:user, admin: true, can_create_group: true)
expect(response).to have_gitlab_http_status(:created)
user_id = json_response['id']
new_user = User.find(user_id)
@@ -1121,13 +1166,13 @@ RSpec.describe API::Users, feature_category: :user_profile do
optional_attributes = { confirm: true, theme_id: 2, color_scheme_id: 4 }
attributes = attributes_for(:user).merge(optional_attributes)
- post api('/users', admin), params: attributes
+ post api(path, admin, admin_mode: true), params: attributes
expect(response).to have_gitlab_http_status(:created)
end
it "creates non-admin user" do
- post api('/users', admin), params: attributes_for(:user, admin: false, can_create_group: false)
+ post api(path, admin, admin_mode: true), params: attributes_for(:user, admin: false, can_create_group: false)
expect(response).to have_gitlab_http_status(:created)
user_id = json_response['id']
new_user = User.find(user_id)
@@ -1136,7 +1181,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "creates non-admin users by default" do
- post api('/users', admin), params: attributes_for(:user)
+ post api(path, admin, admin_mode: true), params: attributes_for(:user)
expect(response).to have_gitlab_http_status(:created)
user_id = json_response['id']
new_user = User.find(user_id)
@@ -1144,13 +1189,13 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "returns 201 Created on success" do
- post api("/users", admin), params: attributes_for(:user, projects_limit: 3)
+ post api(path, admin, admin_mode: true), params: attributes_for(:user, projects_limit: 3)
expect(response).to match_response_schema('public_api/v4/user/admin')
expect(response).to have_gitlab_http_status(:created)
end
it 'creates non-external users by default' do
- post api("/users", admin), params: attributes_for(:user)
+ post api(path, admin, admin_mode: true), params: attributes_for(:user)
expect(response).to have_gitlab_http_status(:created)
user_id = json_response['id']
@@ -1159,7 +1204,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'allows an external user to be created' do
- post api("/users", admin), params: attributes_for(:user, external: true)
+ post api(path, admin, admin_mode: true), params: attributes_for(:user, external: true)
expect(response).to have_gitlab_http_status(:created)
user_id = json_response['id']
@@ -1168,7 +1213,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "creates user with reset password" do
- post api('/users', admin), params: attributes_for(:user, reset_password: true).except(:password)
+ post api(path, admin, admin_mode: true), params: attributes_for(:user, reset_password: true).except(:password)
expect(response).to have_gitlab_http_status(:created)
@@ -1181,7 +1226,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it "creates user with random password" do
params = attributes_for(:user, force_random_password: true)
params.delete(:password)
- post api('/users', admin), params: params
+ post api(path, admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:created)
@@ -1192,7 +1237,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "creates user with private profile" do
- post api('/users', admin), params: attributes_for(:user, private_profile: true)
+ post api(path, admin, admin_mode: true), params: attributes_for(:user, private_profile: true)
expect(response).to have_gitlab_http_status(:created)
@@ -1204,7 +1249,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "creates user with view_diffs_file_by_file" do
- post api('/users', admin), params: attributes_for(:user, view_diffs_file_by_file: true)
+ post api(path, admin, admin_mode: true), params: attributes_for(:user, view_diffs_file_by_file: true)
expect(response).to have_gitlab_http_status(:created)
@@ -1217,7 +1262,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it "creates user with avatar" do
workhorse_form_with_file(
- api('/users', admin),
+ api(path, admin, admin_mode: true),
method: :post,
file_key: :avatar,
params: attributes_for(:user, avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif'))
@@ -1232,7 +1277,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "does not create user with invalid email" do
- post api('/users', admin),
+ post api(path, admin, admin_mode: true),
params: {
email: 'invalid email',
password: User.random_password,
@@ -1242,22 +1287,22 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'returns 400 error if name not given' do
- post api('/users', admin), params: attributes_for(:user).except(:name)
+ post api(path, admin, admin_mode: true), params: attributes_for(:user).except(:name)
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'returns 400 error if password not given' do
- post api('/users', admin), params: attributes_for(:user).except(:password)
+ post api(path, admin, admin_mode: true), params: attributes_for(:user).except(:password)
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'returns 400 error if email not given' do
- post api('/users', admin), params: attributes_for(:user).except(:email)
+ post api(path, admin, admin_mode: true), params: attributes_for(:user).except(:email)
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'returns 400 error if username not given' do
- post api('/users', admin), params: attributes_for(:user).except(:username)
+ post api(path, admin, admin_mode: true), params: attributes_for(:user).except(:username)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -1265,13 +1310,13 @@ RSpec.describe API::Users, feature_category: :user_profile do
optional_attributes = { theme_id: 50, color_scheme_id: 50 }
attributes = attributes_for(:user).merge(optional_attributes)
- post api('/users', admin), params: attributes
+ post api(path, admin, admin_mode: true), params: attributes
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'returns 400 error if user does not validate' do
- post api('/users', admin),
+ post api(path, admin, admin_mode: true),
params: {
password: 'pass',
email: 'test@example.com',
@@ -1288,12 +1333,12 @@ RSpec.describe API::Users, feature_category: :user_profile do
expect(json_response['message']['projects_limit'])
.to eq(['must be greater than or equal to 0'])
expect(json_response['message']['username'])
- .to eq([Gitlab::PathRegex.namespace_format_message])
+ .to match_array([Gitlab::PathRegex.namespace_format_message, Gitlab::Regex.oci_repository_path_regex_message])
end
it 'tracks weak password errors' do
attributes = attributes_for(:user).merge({ password: "password" })
- post api('/users', admin), params: attributes
+ post api(path, admin, admin_mode: true), params: attributes
expect(json_response['message']['password'])
.to eq(['must not contain commonly used combinations of words and letters'])
@@ -1306,13 +1351,13 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "is not available for non admin users" do
- post api("/users", user), params: attributes_for(:user)
+ post api(path, user), params: attributes_for(:user)
expect(response).to have_gitlab_http_status(:forbidden)
end
context 'with existing user' do
before do
- post api('/users', admin),
+ post api(path, admin, admin_mode: true),
params: {
email: 'test@example.com',
password: User.random_password,
@@ -1323,7 +1368,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns 409 conflict error if user with same email exists' do
expect do
- post api('/users', admin),
+ post api(path, admin, admin_mode: true),
params: {
name: 'foo',
email: 'test@example.com',
@@ -1337,7 +1382,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns 409 conflict error if same username exists' do
expect do
- post api('/users', admin),
+ post api(path, admin, admin_mode: true),
params: {
name: 'foo',
email: 'foo@example.com',
@@ -1351,7 +1396,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns 409 conflict error if same username exists (case insensitive)' do
expect do
- post api('/users', admin),
+ post api(path, admin, admin_mode: true),
params: {
name: 'foo',
email: 'foo@example.com',
@@ -1364,7 +1409,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'creates user with new identity' do
- post api("/users", admin), params: attributes_for(:user, provider: 'github', extern_uid: '67890')
+ post api(path, admin, admin_mode: true), params: attributes_for(:user, provider: 'github', extern_uid: '67890')
expect(response).to have_gitlab_http_status(:created)
expect(json_response['identities'].first['extern_uid']).to eq('67890')
@@ -1378,7 +1423,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns 409 conflict error' do
expect do
- post api('/users', admin),
+ post api(path, admin, admin_mode: true),
params: {
name: 'foo',
email: confirmed_user.email,
@@ -1396,7 +1441,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns 409 conflict error' do
expect do
- post api('/users', admin),
+ post api(path, admin, admin_mode: true),
params: {
name: 'foo',
email: unconfirmed_user.email,
@@ -1416,7 +1461,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns 409 conflict error' do
expect do
- post api('/users', admin),
+ post api(path, admin, admin_mode: true),
params: {
name: 'foo',
email: email.email,
@@ -1434,7 +1479,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'does not create user' do
expect do
- post api('/users', admin),
+ post api(path, admin, admin_mode: true),
params: {
name: 'foo',
email: email.email,
@@ -1465,7 +1510,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
shared_examples_for 'creates the user with the value of `private_profile` based on the application setting' do
specify do
- post api("/users", admin), params: params
+ post api(path, admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:created)
user = User.find_by(id: json_response['id'], private_profile: true)
@@ -1479,7 +1524,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'when the attribute is overridden in params' do
it 'creates the user with the value of `private_profile` same as the value of the overridden param' do
- post api("/users", admin), params: params.merge(private_profile: false)
+ post api(path, admin, admin_mode: true), params: params.merge(private_profile: false)
expect(response).to have_gitlab_http_status(:created)
user = User.find_by(id: json_response['id'], private_profile: false)
@@ -1497,8 +1542,14 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe "PUT /users/:id" do
+ let(:path) { "/users/#{user.id}" }
+
+ it_behaves_like 'PUT request permissions for admin mode' do
+ let(:params) { { bio: 'new test bio' } }
+ end
+
it "returns 200 OK on success" do
- put api("/users/#{user.id}", admin), params: { bio: 'new test bio' }
+ put api(path, admin, admin_mode: true), params: { bio: 'new test bio' }
expect(response).to match_response_schema('public_api/v4/user/admin')
expect(response).to have_gitlab_http_status(:ok)
@@ -1506,7 +1557,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'updating password' do
def update_password(user, admin, password = User.random_password)
- put api("/users/#{user.id}", admin), params: { password: password }
+ put api("/users/#{user.id}", admin, admin_mode: true), params: { password: password }
end
context 'admin updates their own password' do
@@ -1564,7 +1615,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "updates user with new bio" do
- put api("/users/#{user.id}", admin), params: { bio: 'new test bio' }
+ put api(path, admin, admin_mode: true), params: { bio: 'new test bio' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['bio']).to eq('new test bio')
@@ -1574,7 +1625,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it "updates user with empty bio" do
user.update!(bio: 'previous bio')
- put api("/users/#{user.id}", admin), params: { bio: '' }
+ put api(path, admin, admin_mode: true), params: { bio: '' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['bio']).to eq('')
@@ -1582,7 +1633,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'updates user with nil bio' do
- put api("/users/#{user.id}", admin), params: { bio: nil }
+ put api(path, admin, admin_mode: true), params: { bio: nil }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['bio']).to eq('')
@@ -1590,7 +1641,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "updates user with organization" do
- put api("/users/#{user.id}", admin), params: { organization: 'GitLab' }
+ put api(path, admin, admin_mode: true), params: { organization: 'GitLab' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['organization']).to eq('GitLab')
@@ -1599,7 +1650,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'updates user with avatar' do
workhorse_form_with_file(
- api("/users/#{user.id}", admin),
+ api(path, admin, admin_mode: true),
method: :put,
file_key: :avatar,
params: { avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') }
@@ -1615,7 +1666,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'updates user with a new email' do
old_email = user.email
old_notification_email = user.notification_email_or_default
- put api("/users/#{user.id}", admin), params: { email: 'new@email.com' }
+ put api(path, admin, admin_mode: true), params: { email: 'new@email.com' }
user.reload
@@ -1627,7 +1678,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'skips reconfirmation when requested' do
- put api("/users/#{user.id}", admin), params: { email: 'new@email.com', skip_reconfirmation: true }
+ put api(path, admin, admin_mode: true), params: { email: 'new@email.com', skip_reconfirmation: true }
user.reload
@@ -1637,7 +1688,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'updates user with their own username' do
- put api("/users/#{user.id}", admin), params: { username: user.username }
+ put api(path, admin, admin_mode: true), params: { username: user.username }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['username']).to eq(user.username)
@@ -1645,14 +1696,14 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "updates user's existing identity" do
- put api("/users/#{ldap_user.id}", admin), params: { provider: 'ldapmain', extern_uid: '654321' }
+ put api("/users/#{ldap_user.id}", admin, admin_mode: true), params: { provider: 'ldapmain', extern_uid: '654321' }
expect(response).to have_gitlab_http_status(:ok)
expect(ldap_user.reload.identities.first.extern_uid).to eq('654321')
end
it 'updates user with new identity' do
- put api("/users/#{user.id}", admin), params: { provider: 'github', extern_uid: 'john' }
+ put api(path, admin, admin_mode: true), params: { provider: 'github', extern_uid: 'john' }
expect(response).to have_gitlab_http_status(:ok)
expect(user.reload.identities.first.extern_uid).to eq('john')
@@ -1660,14 +1711,14 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "updates admin status" do
- put api("/users/#{user.id}", admin), params: { admin: true }
+ put api(path, admin, admin_mode: true), params: { admin: true }
expect(response).to have_gitlab_http_status(:ok)
expect(user.reload.admin).to eq(true)
end
it "updates external status" do
- put api("/users/#{user.id}", admin), params: { external: true }
+ put api(path, admin, admin_mode: true), params: { external: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['external']).to eq(true)
@@ -1675,14 +1726,14 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "does have default values for theme and color-scheme ID" do
- put api("/users/#{user.id}", admin), params: {}
+ put api(path, admin, admin_mode: true), params: {}
expect(user.reload.theme_id).to eq(Gitlab::Themes.default.id)
expect(user.reload.color_scheme_id).to eq(Gitlab::ColorSchemes.default.id)
end
it "updates viewing diffs file by file" do
- put api("/users/#{user.id}", admin), params: { view_diffs_file_by_file: true }
+ put api(path, admin, admin_mode: true), params: { view_diffs_file_by_file: true }
expect(response).to have_gitlab_http_status(:ok)
expect(user.reload.user_preference.view_diffs_file_by_file?).to eq(true)
@@ -1693,7 +1744,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
current_value = user.private_profile
new_value = !current_value
- put api("/users/#{user.id}", admin), params: { private_profile: new_value }
+ put api(path, admin, admin_mode: true), params: { private_profile: new_value }
expect(response).to have_gitlab_http_status(:ok)
expect(user.reload.private_profile).to eq(new_value)
@@ -1707,7 +1758,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it "updates private_profile to value of the application setting" do
user.update!(private_profile: false)
- put api("/users/#{user.id}", admin), params: { private_profile: nil }
+ put api(path, admin, admin_mode: true), params: { private_profile: nil }
expect(response).to have_gitlab_http_status(:ok)
expect(user.reload.private_profile).to eq(true)
@@ -1717,7 +1768,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it "does not modify private profile when field is not provided" do
user.update!(private_profile: true)
- put api("/users/#{user.id}", admin), params: {}
+ put api(path, admin, admin_mode: true), params: {}
expect(response).to have_gitlab_http_status(:ok)
expect(user.reload.private_profile).to eq(true)
@@ -1730,7 +1781,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
user.update!(theme_id: theme.id, color_scheme_id: scheme.id)
- put api("/users/#{user.id}", admin), params: {}
+ put api(path, admin, admin_mode: true), params: {}
expect(response).to have_gitlab_http_status(:ok)
expect(user.reload.theme_id).to eq(theme.id)
@@ -1740,7 +1791,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it "does not update admin status" do
admin_user = create(:admin)
- put api("/users/#{admin_user.id}", admin), params: { can_create_group: false }
+ put api("/users/#{admin_user.id}", admin, admin_mode: true), params: { can_create_group: false }
expect(response).to have_gitlab_http_status(:ok)
expect(admin_user.reload.admin).to eq(true)
@@ -1748,35 +1799,35 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "does not allow invalid update" do
- put api("/users/#{user.id}", admin), params: { email: 'invalid email' }
+ put api(path, admin, admin_mode: true), params: { email: 'invalid email' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(user.reload.email).not_to eq('invalid email')
end
it "updates theme id" do
- put api("/users/#{user.id}", admin), params: { theme_id: 5 }
+ put api(path, admin, admin_mode: true), params: { theme_id: 5 }
expect(response).to have_gitlab_http_status(:ok)
expect(user.reload.theme_id).to eq(5)
end
it "does not update invalid theme id" do
- put api("/users/#{user.id}", admin), params: { theme_id: 50 }
+ put api(path, admin, admin_mode: true), params: { theme_id: 50 }
expect(response).to have_gitlab_http_status(:bad_request)
expect(user.reload.theme_id).not_to eq(50)
end
it "updates color scheme id" do
- put api("/users/#{user.id}", admin), params: { color_scheme_id: 5 }
+ put api(path, admin, admin_mode: true), params: { color_scheme_id: 5 }
expect(response).to have_gitlab_http_status(:ok)
expect(user.reload.color_scheme_id).to eq(5)
end
it "does not update invalid color scheme id" do
- put api("/users/#{user.id}", admin), params: { color_scheme_id: 50 }
+ put api(path, admin, admin_mode: true), params: { color_scheme_id: 50 }
expect(response).to have_gitlab_http_status(:bad_request)
expect(user.reload.color_scheme_id).not_to eq(50)
@@ -1785,7 +1836,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'when the current user is not an admin' do
it "is not available" do
expect do
- put api("/users/#{user.id}", user), params: attributes_for(:user)
+ put api(path, user), params: attributes_for(:user)
end.not_to change { user.reload.attributes }
expect(response).to have_gitlab_http_status(:forbidden)
@@ -1793,20 +1844,20 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "returns 404 for non-existing user" do
- put api("/users/0", admin), params: { bio: 'update should fail' }
+ put api("/users/#{non_existing_record_id}", admin, admin_mode: true), params: { bio: 'update should fail' }
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
end
it "returns a 404 if invalid ID" do
- put api("/users/ASDF", admin)
+ put api("/users/ASDF", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns 400 error if user does not validate' do
- put api("/users/#{user.id}", admin),
+ put api(path, admin, admin_mode: true),
params: {
password: 'pass',
email: 'test@example.com',
@@ -1823,30 +1874,30 @@ RSpec.describe API::Users, feature_category: :user_profile do
expect(json_response['message']['projects_limit'])
.to eq(['must be greater than or equal to 0'])
expect(json_response['message']['username'])
- .to eq([Gitlab::PathRegex.namespace_format_message])
+ .to match_array([Gitlab::PathRegex.namespace_format_message, Gitlab::Regex.oci_repository_path_regex_message])
end
it 'returns 400 if provider is missing for identity update' do
- put api("/users/#{omniauth_user.id}", admin), params: { extern_uid: '654321' }
+ put api("/users/#{omniauth_user.id}", admin, admin_mode: true), params: { extern_uid: '654321' }
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'returns 400 if external UID is missing for identity update' do
- put api("/users/#{omniauth_user.id}", admin), params: { provider: 'ldap' }
+ put api("/users/#{omniauth_user.id}", admin, admin_mode: true), params: { provider: 'ldap' }
expect(response).to have_gitlab_http_status(:bad_request)
end
context "with existing user" do
before do
- post api("/users", admin), params: { email: 'test@example.com', password: User.random_password, username: 'test', name: 'test' }
- post api("/users", admin), params: { email: 'foo@bar.com', password: User.random_password, username: 'john', name: 'john' }
+ post api("/users", admin, admin_mode: true), params: { email: 'test@example.com', password: User.random_password, username: 'test', name: 'test' }
+ post api("/users", admin, admin_mode: true), params: { email: 'foo@bar.com', password: User.random_password, username: 'john', name: 'john' }
@user = User.all.last
end
it 'returns 409 conflict error if email address exists' do
- put api("/users/#{@user.id}", admin), params: { email: 'test@example.com' }
+ put api("/users/#{@user.id}", admin, admin_mode: true), params: { email: 'test@example.com' }
expect(response).to have_gitlab_http_status(:conflict)
expect(@user.reload.email).to eq(@user.email)
@@ -1854,7 +1905,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns 409 conflict error if username taken' do
@user_id = User.all.last.id
- put api("/users/#{@user.id}", admin), params: { username: 'test' }
+ put api("/users/#{@user.id}", admin, admin_mode: true), params: { username: 'test' }
expect(response).to have_gitlab_http_status(:conflict)
expect(@user.reload.username).to eq(@user.username)
@@ -1862,7 +1913,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns 409 conflict error if username taken (case insensitive)' do
@user_id = User.all.last.id
- put api("/users/#{@user.id}", admin), params: { username: 'TEST' }
+ put api("/users/#{@user.id}", admin, admin_mode: true), params: { username: 'TEST' }
expect(response).to have_gitlab_http_status(:conflict)
expect(@user.reload.username).to eq(@user.username)
@@ -1874,7 +1925,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
let!(:confirmed_user) { create(:user, email: 'foo@example.com') }
it 'returns 409 conflict error' do
- put api("/users/#{user.id}", admin), params: { email: confirmed_user.email }
+ put api(path, admin, admin_mode: true), params: { email: confirmed_user.email }
expect(response).to have_gitlab_http_status(:conflict)
expect(user.reload.email).not_to eq(confirmed_user.email)
@@ -1885,7 +1936,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
let!(:unconfirmed_user) { create(:user, :unconfirmed, email: 'foo@example.com') }
it 'returns 409 conflict error' do
- put api("/users/#{user.id}", admin), params: { email: unconfirmed_user.email }
+ put api(path, admin, admin_mode: true), params: { email: unconfirmed_user.email }
expect(response).to have_gitlab_http_status(:conflict)
expect(user.reload.email).not_to eq(unconfirmed_user.email)
@@ -1898,7 +1949,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
let!(:email) { create(:email, :confirmed, email: 'foo@example.com') }
it 'returns 409 conflict error' do
- put api("/users/#{user.id}", admin), params: { email: email.email }
+ put api(path, admin, admin_mode: true), params: { email: email.email }
expect(response).to have_gitlab_http_status(:conflict)
expect(user.reload.email).not_to eq(email.email)
@@ -1909,7 +1960,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
let!(:email) { create(:email, email: 'foo@example.com') }
it 'does not update email' do
- put api("/users/#{user.id}", admin), params: { email: email.email }
+ put api(path, admin, admin_mode: true), params: { email: email.email }
expect(response).to have_gitlab_http_status(:bad_request)
expect(user.reload.email).not_to eq(email.email)
@@ -1921,6 +1972,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
describe "PUT /user/:id/credit_card_validation" do
let(:credit_card_validated_time) { Time.utc(2020, 1, 1) }
let(:expiration_year) { Date.today.year + 10 }
+ let(:path) { "/user/#{user.id}/credit_card_validation" }
let(:params) do
{
credit_card_validated_at: credit_card_validated_time,
@@ -1932,25 +1984,27 @@ RSpec.describe API::Users, feature_category: :user_profile do
}
end
+ it_behaves_like 'PUT request permissions for admin mode'
+
context 'when unauthenticated' do
it 'returns authentication error' do
- put api("/user/#{user.id}/credit_card_validation"), params: {}
+ put api(path), params: {}
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'when authenticated as non-admin' do
- it "does not allow updating user's credit card validation", :aggregate_failures do
- put api("/user/#{user.id}/credit_card_validation", user), params: params
+ it "does not allow updating user's credit card validation" do
+ put api(path, user), params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when authenticated as admin' do
- it "updates user's credit card validation", :aggregate_failures do
- put api("/user/#{user.id}/credit_card_validation", admin), params: params
+ it "updates user's credit card validation" do
+ put api(path, admin, admin_mode: true), params: params
user.reload
@@ -1965,13 +2019,13 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "returns 400 error if credit_card_validated_at is missing" do
- put api("/user/#{user.id}/credit_card_validation", admin), params: {}
+ put api(path, admin, admin_mode: true), params: {}
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'returns 404 error if user not found' do
- put api("/user/#{non_existing_record_id}/credit_card_validation", admin), params: params
+ put api("/user/#{non_existing_record_id}/credit_card_validation", admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
@@ -1981,10 +2035,13 @@ RSpec.describe API::Users, feature_category: :user_profile do
describe "DELETE /users/:id/identities/:provider" do
let(:test_user) { create(:omniauth_user, provider: 'ldapmain') }
+ let(:path) { "/users/#{test_user.id}/identities/ldapmain" }
+
+ it_behaves_like 'DELETE request permissions for admin mode'
context 'when unauthenticated' do
it 'returns authentication error' do
- delete api("/users/#{test_user.id}/identities/ldapmain")
+ delete api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -1993,24 +2050,24 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'when authenticated' do
it 'deletes identity of given provider' do
expect do
- delete api("/users/#{test_user.id}/identities/ldapmain", admin)
+ delete api(path, admin, admin_mode: true)
end.to change { test_user.identities.count }.by(-1)
expect(response).to have_gitlab_http_status(:no_content)
end
it_behaves_like '412 response' do
- let(:request) { api("/users/#{test_user.id}/identities/ldapmain", admin) }
+ let(:request) { api(path, admin, admin_mode: true) }
end
it 'returns 404 error if user not found' do
- delete api("/users/0/identities/ldapmain", admin)
+ delete api("/users/#{non_existing_record_id}/identities/ldapmain", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns 404 error if identity not found' do
- delete api("/users/#{test_user.id}/identities/saml", admin)
+ delete api("/users/#{test_user.id}/identities/saml", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Identity Not Found')
@@ -2019,25 +2076,31 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe "POST /users/:id/keys" do
+ let(:path) { "/users/#{user.id}/keys" }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { attributes_for(:key, usage_type: :signing) }
+ end
+
it "does not create invalid ssh key" do
- post api("/users/#{user.id}/keys", admin), params: { title: "invalid key" }
+ post api(path, admin, admin_mode: true), params: { title: "invalid key" }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('key is missing')
end
it 'does not create key without title' do
- post api("/users/#{user.id}/keys", admin), params: { key: 'some key' }
+ post api(path, admin, admin_mode: true), params: { key: 'some key' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('title is missing')
end
- it "creates ssh key", :aggregate_failures do
+ it "creates ssh key" do
key_attrs = attributes_for(:key, usage_type: :signing)
expect do
- post api("/users/#{user.id}/keys", admin), params: key_attrs
+ post api(path, admin, admin_mode: true), params: key_attrs
end.to change { user.keys.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -2052,20 +2115,21 @@ RSpec.describe API::Users, feature_category: :user_profile do
optional_attributes = { expires_at: 3.weeks.from_now }
attributes = attributes_for(:key).merge(optional_attributes)
- post api("/users/#{user.id}/keys", admin), params: attributes
+ post api(path, admin, admin_mode: true), params: attributes
expect(response).to have_gitlab_http_status(:created)
expect(json_response['expires_at'].to_date).to eq(optional_attributes[:expires_at].to_date)
end
it "returns 400 for invalid ID" do
- post api("/users/0/keys", admin)
+ post api("/users/#{non_existing_record_id}/keys", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
end
describe 'GET /users/:id/project_deploy_keys' do
let(:project) { create(:project) }
+ let(:path) { "/users/#{user.id}/project_deploy_keys" }
before do
project.add_maintainer(user)
@@ -2082,7 +2146,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'returns array of project deploy keys with pagination' do
- get api("/users/#{user.id}/project_deploy_keys", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -2094,7 +2158,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
dev_user = create(:user)
project.add_developer(dev_user)
- get api("/users/#{user.id}/project_deploy_keys", dev_user)
+ get api(path, dev_user)
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['message']).to eq('403 Forbidden - No common authorized project found')
@@ -2113,7 +2177,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'when no common projects for user and current_user' do
it 'forbids' do
- get api("/users/#{user.id}/project_deploy_keys", second_user)
+ get api(path, second_user)
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['message']).to eq('403 Forbidden - No common authorized project found')
@@ -2125,11 +2189,13 @@ RSpec.describe API::Users, feature_category: :user_profile do
project.add_maintainer(second_user)
end
+ let(:path) { "/users/#{second_user.id}/project_deploy_keys" }
+
it 'lists only common project keys' do
expect(second_user.project_deploy_keys).to contain_exactly(
project.deploy_keys.first, second_project.deploy_keys.first)
- get api("/users/#{second_user.id}/project_deploy_keys", user)
+ get api(path, user)
expect(json_response.count).to eq(1)
expect(json_response.first['key']).to eq(project.deploy_keys.first.key)
@@ -2144,7 +2210,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
create(:deploy_key, user: second_user)
create(:deploy_key, user: third_user)
- get api("/users/#{second_user.id}/project_deploy_keys", third_user)
+ get api(path, third_user)
expect(json_response.count).to eq(2)
expect([json_response.first['key'], json_response.second['key']]).to contain_exactly(
@@ -2155,14 +2221,14 @@ RSpec.describe API::Users, feature_category: :user_profile do
second_project.add_maintainer(user)
control_count = ActiveRecord::QueryRecorder.new do
- get api("/users/#{second_user.id}/project_deploy_keys", user)
+ get api(path, user)
end.count
deploy_key = create(:deploy_key, user: second_user)
create(:deploy_keys_project, project: second_project, deploy_key_id: deploy_key.id)
expect do
- get api("/users/#{second_user.id}/project_deploy_keys", user)
+ get api(path, user)
end.not_to exceed_query_limit(control_count)
end
end
@@ -2170,6 +2236,10 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe 'GET /user/:id/keys' do
+ subject(:request) { get api(path) }
+
+ let(:path) { "/users/#{user.id}/keys" }
+
it 'returns 404 for non-existing user' do
get api("/users/#{non_existing_record_id}/keys")
@@ -2180,7 +2250,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns array of ssh keys' do
user.keys << key
- get api("/users/#{user.id}/keys")
+ request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -2190,7 +2260,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns array of ssh keys with comments replaced with'\
'a simple identifier of username + hostname' do
- get api("/users/#{user.id}/keys")
+ request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -2202,24 +2272,26 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'N+1 queries' do
before do
- get api("/users/#{user.id}/keys")
+ request
end
it 'avoids N+1 queries', :request_store do
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api("/users/#{user.id}/keys")
+ request
end.count
create_list(:key, 2, user: user)
expect do
- get api("/users/#{user.id}/keys")
+ request
end.not_to exceed_all_query_limit(control_count)
end
end
end
describe 'GET /user/:user_id/keys' do
+ let(:path) { "/users/#{user.username}/keys" }
+
it 'returns 404 for non-existing user' do
get api("/users/#{non_existing_record_id}/keys")
@@ -2230,7 +2302,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns array of ssh keys' do
user.keys << key
- get api("/users/#{user.username}/keys")
+ get api(path)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -2240,25 +2312,27 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe 'GET /user/:id/keys/:key_id' do
- it 'gets existing key', :aggregate_failures do
+ let(:path) { "/users/#{user.id}/keys/#{key.id}" }
+
+ it 'gets existing key' do
user.keys << key
- get api("/users/#{user.id}/keys/#{key.id}")
+ get api(path)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['title']).to eq(key.title)
end
- it 'returns 404 error if user not found', :aggregate_failures do
+ it 'returns 404 error if user not found' do
user.keys << key
- get api("/users/0/keys/#{key.id}")
+ get api("/users/#{non_existing_record_id}/keys/#{key.id}")
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
end
- it 'returns 404 error if key not found', :aggregate_failures do
+ it 'returns 404 error if key not found' do
get api("/users/#{user.id}/keys/#{non_existing_record_id}")
expect(response).to have_gitlab_http_status(:not_found)
@@ -2267,6 +2341,10 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe 'DELETE /user/:id/keys/:key_id' do
+ let(:path) { "/users/#{user.id}/keys/#{key.id}" }
+
+ it_behaves_like 'DELETE request permissions for admin mode'
+
context 'when unauthenticated' do
it 'returns authentication error' do
delete api("/users/#{user.id}/keys/#{non_existing_record_id}")
@@ -2279,26 +2357,26 @@ RSpec.describe API::Users, feature_category: :user_profile do
user.keys << key
expect do
- delete api("/users/#{user.id}/keys/#{key.id}", admin)
+ delete api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
end.to change { user.keys.count }.by(-1)
end
it_behaves_like '412 response' do
- let(:request) { api("/users/#{user.id}/keys/#{key.id}", admin) }
+ let(:request) { api(path, admin, admin_mode: true) }
end
it 'returns 404 error if user not found' do
user.keys << key
- delete api("/users/0/keys/#{key.id}", admin)
+ delete api("/users/#{non_existing_record_id}/keys/#{key.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns 404 error if key not foud' do
- delete api("/users/#{user.id}/keys/#{non_existing_record_id}", admin)
+ delete api("/users/#{user.id}/keys/#{non_existing_record_id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Key Not Found')
end
@@ -2306,8 +2384,14 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe 'POST /users/:id/gpg_keys' do
+ let(:path) { "/users/#{user.id}/gpg_keys" }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { attributes_for :gpg_key, key: GpgHelpers::User2.public_key }
+ end
+
it 'does not create invalid GPG key' do
- post api("/users/#{user.id}/gpg_keys", admin)
+ post api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('key is missing')
@@ -2317,22 +2401,24 @@ RSpec.describe API::Users, feature_category: :user_profile do
key_attrs = attributes_for :gpg_key, key: GpgHelpers::User2.public_key
expect do
- post api("/users/#{user.id}/gpg_keys", admin), params: key_attrs
+ post api(path, admin, admin_mode: true), params: key_attrs
expect(response).to have_gitlab_http_status(:created)
end.to change { user.gpg_keys.count }.by(1)
end
it 'returns 400 for invalid ID' do
- post api('/users/0/gpg_keys', admin)
+ post api("/users/#{non_existing_record_id}/gpg_keys", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
end
describe 'GET /user/:id/gpg_keys' do
+ let(:path) { "/users/#{user.id}/gpg_keys" }
+
it 'returns 404 for non-existing user' do
- get api('/users/0/gpg_keys')
+ get api("/users/#{non_existing_record_id}/gpg_keys")
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
@@ -2341,7 +2427,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns array of GPG keys' do
user.gpg_keys << gpg_key
- get api("/users/#{user.id}/gpg_keys")
+ get api(path)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -2351,15 +2437,17 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe 'GET /user/:id/gpg_keys/:key_id' do
+ let(:path) { "/users/#{user.id}/gpg_keys/#{gpg_key.id}" }
+
it 'returns 404 for non-existing user' do
- get api('/users/0/gpg_keys/1')
+ get api("/users/#{non_existing_record_id}/gpg_keys/1")
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns 404 for non-existing key' do
- get api("/users/#{user.id}/gpg_keys/0")
+ get api("/users/#{user.id}/gpg_keys/#{non_existing_record_id}")
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 GPG Key Not Found')
@@ -2368,7 +2456,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns a single GPG key' do
user.gpg_keys << gpg_key
- get api("/users/#{user.id}/gpg_keys/#{gpg_key.id}")
+ get api(path)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['key']).to eq(gpg_key.key)
@@ -2376,6 +2464,10 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe 'DELETE /user/:id/gpg_keys/:key_id' do
+ let(:path) { "/users/#{user.id}/gpg_keys/#{gpg_key.id}" }
+
+ it_behaves_like 'DELETE request permissions for admin mode'
+
context 'when unauthenticated' do
it 'returns authentication error' do
delete api("/users/#{user.id}/keys/#{non_existing_record_id}")
@@ -2389,7 +2481,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
user.gpg_keys << gpg_key
expect do
- delete api("/users/#{user.id}/gpg_keys/#{gpg_key.id}", admin)
+ delete api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
end.to change { user.gpg_keys.count }.by(-1)
@@ -2398,14 +2490,14 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns 404 error if user not found' do
user.keys << key
- delete api("/users/0/gpg_keys/#{gpg_key.id}", admin)
+ delete api("/users/#{non_existing_record_id}/gpg_keys/#{gpg_key.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns 404 error if key not foud' do
- delete api("/users/#{user.id}/gpg_keys/#{non_existing_record_id}", admin)
+ delete api("/users/#{user.id}/gpg_keys/#{non_existing_record_id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 GPG Key Not Found')
@@ -2414,6 +2506,13 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe 'POST /user/:id/gpg_keys/:key_id/revoke' do
+ let(:path) { "/users/#{user.id}/gpg_keys/#{gpg_key.id}/revoke" }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { {} }
+ let(:success_status_code) { :accepted }
+ end
+
context 'when unauthenticated' do
it 'returns authentication error' do
post api("/users/#{user.id}/gpg_keys/#{non_existing_record_id}/revoke")
@@ -2427,7 +2526,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
user.gpg_keys << gpg_key
expect do
- post api("/users/#{user.id}/gpg_keys/#{gpg_key.id}/revoke", admin)
+ post api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:accepted)
end.to change { user.gpg_keys.count }.by(-1)
@@ -2436,14 +2535,14 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns 404 error if user not found' do
user.gpg_keys << gpg_key
- post api("/users/0/gpg_keys/#{gpg_key.id}/revoke", admin)
+ post api("/users/#{non_existing_record_id}/gpg_keys/#{gpg_key.id}/revoke", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns 404 error if key not foud' do
- post api("/users/#{user.id}/gpg_keys/#{non_existing_record_id}/revoke", admin)
+ post api("/users/#{user.id}/gpg_keys/#{non_existing_record_id}/revoke", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 GPG Key Not Found')
@@ -2452,8 +2551,19 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe "POST /users/:id/emails", :mailer do
+ let(:path) { "/users/#{user.id}/emails" }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ before do
+ email_attrs[:skip_confirmation] = true
+ end
+
+ let(:email_attrs) { attributes_for :email }
+ let(:params) { email_attrs }
+ end
+
it "does not create invalid email" do
- post api("/users/#{user.id}/emails", admin), params: {}
+ post api(path, admin, admin_mode: true), params: {}
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('email is missing')
@@ -2464,7 +2574,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
perform_enqueued_jobs do
expect do
- post api("/users/#{user.id}/emails", admin), params: email_attrs
+ post api(path, admin, admin_mode: true), params: email_attrs
end.to change { user.emails.count }.by(1)
end
@@ -2473,7 +2583,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "returns a 400 for invalid ID" do
- post api("/users/0/emails", admin)
+ post api("/users/#{non_existing_record_id}/emails", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -2482,7 +2592,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
email_attrs = attributes_for :email
email_attrs[:skip_confirmation] = true
- post api("/users/#{user.id}/emails", admin), params: email_attrs
+ post api(path, admin, admin_mode: true), params: email_attrs
expect(response).to have_gitlab_http_status(:created)
@@ -2494,7 +2604,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
let!(:confirmed_user) { create(:user, email: 'foo@example.com') }
it 'returns 400 error' do
- post api("/users/#{user.id}/emails", admin), params: { email: confirmed_user.email }
+ post api(path, admin, admin_mode: true), params: { email: confirmed_user.email }
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -2504,7 +2614,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
let!(:unconfirmed_user) { create(:user, :unconfirmed, email: 'foo@example.com') }
it 'returns 400 error' do
- post api("/users/#{user.id}/emails", admin), params: { email: unconfirmed_user.email }
+ post api(path, admin, admin_mode: true), params: { email: unconfirmed_user.email }
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -2516,7 +2626,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
let!(:email) { create(:email, :confirmed, email: 'foo@example.com') }
it 'returns 400 error' do
- post api("/users/#{user.id}/emails", admin), params: { email: email.email }
+ post api(path, admin, admin_mode: true), params: { email: email.email }
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -2526,7 +2636,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
let!(:email) { create(:email, email: 'foo@example.com') }
it 'returns 400 error' do
- post api("/users/#{user.id}/emails", admin), params: { email: email.email }
+ post api(path, admin, admin_mode: true), params: { email: email.email }
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -2535,16 +2645,18 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe 'GET /user/:id/emails' do
+ let(:path) { "/users/#{user.id}/emails" }
+
context 'when unauthenticated' do
it 'returns authentication error' do
- get api("/users/#{user.id}/emails")
+ get api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'when authenticated' do
it 'returns 404 for non-existing user' do
- get api('/users/0/emails', admin)
+ get api("/users/#{non_existing_record_id}/emails", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -2552,7 +2664,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns array of emails' do
user.emails << email
- get api("/users/#{user.id}/emails", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -2562,7 +2674,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "returns a 404 for invalid ID" do
- get api("/users/ASDF/emails", admin)
+ get api("/users/ASDF/emails", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -2570,6 +2682,10 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe 'DELETE /user/:id/emails/:email_id' do
+ let(:path) { "/users/#{user.id}/emails/#{email.id}" }
+
+ it_behaves_like 'DELETE request permissions for admin mode'
+
context 'when unauthenticated' do
it 'returns authentication error' do
delete api("/users/#{user.id}/emails/#{non_existing_record_id}")
@@ -2582,26 +2698,26 @@ RSpec.describe API::Users, feature_category: :user_profile do
user.emails << email
expect do
- delete api("/users/#{user.id}/emails/#{email.id}", admin)
+ delete api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
end.to change { user.emails.count }.by(-1)
end
it_behaves_like '412 response' do
- let(:request) { api("/users/#{user.id}/emails/#{email.id}", admin) }
+ subject(:request) { api(path, admin, admin_mode: true) }
end
it 'returns 404 error if user not found' do
user.emails << email
- delete api("/users/0/emails/#{email.id}", admin)
+ delete api("/users/#{non_existing_record_id}/emails/#{email.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns 404 error if email not foud' do
- delete api("/users/#{user.id}/emails/#{non_existing_record_id}", admin)
+ delete api("/users/#{user.id}/emails/#{non_existing_record_id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Email Not Found')
end
@@ -2616,9 +2732,12 @@ RSpec.describe API::Users, feature_category: :user_profile do
describe "DELETE /users/:id" do
let_it_be(:issue) { create(:issue, author: user) }
+ let(:path) { "/users/#{user.id}" }
+
+ it_behaves_like 'DELETE request permissions for admin mode'
it "deletes user", :sidekiq_inline do
- perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
+ perform_enqueued_jobs { delete api(path, admin, admin_mode: true) }
expect(response).to have_gitlab_http_status(:no_content)
expect(Users::GhostUserMigration.where(user: user,
@@ -2630,14 +2749,14 @@ RSpec.describe API::Users, feature_category: :user_profile do
context "hard delete disabled" do
it "does not delete user" do
- perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
+ perform_enqueued_jobs { delete api(path, admin, admin_mode: true) }
expect(response).to have_gitlab_http_status(:conflict)
end
end
context "hard delete enabled" do
it "delete user and group", :sidekiq_inline do
- perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) }
+ perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin, admin_mode: true) }
expect(response).to have_gitlab_http_status(:no_content)
expect(Group.exists?(group.id)).to be_falsy
end
@@ -2652,7 +2771,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "delete only user", :sidekiq_inline do
- perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) }
+ perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin, admin_mode: true) }
expect(response).to have_gitlab_http_status(:no_content)
expect(Group.exists?(subgroup.id)).to be_truthy
end
@@ -2661,34 +2780,34 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it_behaves_like '412 response' do
- let(:request) { api("/users/#{user.id}", admin) }
+ let(:request) { api(path, admin, admin_mode: true) }
end
it "does not delete for unauthenticated user" do
- perform_enqueued_jobs { delete api("/users/#{user.id}") }
+ perform_enqueued_jobs { delete api(path) }
expect(response).to have_gitlab_http_status(:unauthorized)
end
it "is not available for non admin users" do
- perform_enqueued_jobs { delete api("/users/#{user.id}", user) }
+ perform_enqueued_jobs { delete api(path, user) }
expect(response).to have_gitlab_http_status(:forbidden)
end
it "returns 404 for non-existing user" do
- perform_enqueued_jobs { delete api("/users/0", admin) }
+ perform_enqueued_jobs { delete api("/users/#{non_existing_record_id}", admin, admin_mode: true) }
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
end
it "returns a 404 for invalid ID" do
- perform_enqueued_jobs { delete api("/users/ASDF", admin) }
+ perform_enqueued_jobs { delete api("/users/ASDF", admin, admin_mode: true) }
expect(response).to have_gitlab_http_status(:not_found)
end
context "hard delete disabled" do
it "moves contributions to the ghost user", :sidekiq_might_not_need_inline do
- perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
+ perform_enqueued_jobs { delete api(path, admin, admin_mode: true) }
expect(response).to have_gitlab_http_status(:no_content)
expect(issue.reload).to be_persisted
@@ -2700,7 +2819,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context "hard delete enabled" do
it "removes contributions", :sidekiq_might_not_need_inline do
- perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) }
+ perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin, admin_mode: true) }
expect(response).to have_gitlab_http_status(:no_content)
expect(Users::GhostUserMigration.where(user: user,
@@ -2711,6 +2830,8 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe "GET /user" do
+ let(:path) { '/user' }
+
shared_examples 'get user info' do |version|
context 'with regular user' do
context 'with personal access token' do
@@ -2724,7 +2845,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'returns current user without private token when sudo not defined' do
- get api("/user", user, version: version)
+ get api(path, user, version: version)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/user/public')
@@ -2732,7 +2853,6 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
context "scopes" do
- let(:path) { "/user" }
let(:api_call) { method(:api) }
include_examples 'allows the "read_user" scope', version
@@ -2740,7 +2860,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
context 'with admin' do
- let(:admin_personal_access_token) { create(:personal_access_token, user: admin).token }
+ let(:admin_personal_access_token) { create(:personal_access_token, :admin_mode, user: admin).token }
context 'with personal access token' do
it 'returns 403 without private token when sudo defined' do
@@ -2761,7 +2881,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'with unauthenticated user' do
it "returns 401 error if user is unauthenticated" do
- get api("/user", version: version)
+ get api(path, version: version)
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -2773,9 +2893,11 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe "GET /user/preferences" do
+ let(:path) { '/user/preferences' }
+
context "when unauthenticated" do
it "returns authentication error" do
- get api("/user/preferences")
+ get api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
@@ -2786,7 +2908,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
user.user_preference.show_whitespace_in_diffs = true
user.save!
- get api("/user/preferences", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response["view_diffs_file_by_file"]).to eq(user.user_preference.view_diffs_file_by_file)
@@ -2796,6 +2918,10 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe "GET /user/keys" do
+ subject(:request) { get api(path, user) }
+
+ let(:path) { "/user/keys" }
+
context "when unauthenticated" do
it "returns authentication error" do
get api("/user/keys")
@@ -2807,7 +2933,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it "returns array of ssh keys" do
user.keys << key
- get api("/user/keys", user)
+ request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -2817,7 +2943,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns array of ssh keys with comments replaced with'\
'a simple identifier of username + hostname' do
- get api("/user/keys", user)
+ request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -2829,24 +2955,23 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'N+1 queries' do
before do
- get api("/user/keys", user)
+ request
end
it 'avoids N+1 queries', :request_store do
control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api("/user/keys", user)
+ request
end.count
create_list(:key, 2, user: user)
expect do
- get api("/user/keys", user)
+ request
end.not_to exceed_all_query_limit(control_count)
end
end
context "scopes" do
- let(:path) { "/user/keys" }
let(:api_call) { method(:api) }
include_examples 'allows the "read_user" scope'
@@ -2855,16 +2980,18 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe "GET /user/keys/:key_id" do
+ let(:path) { "/user/keys/#{key.id}" }
+
it "returns single key" do
user.keys << key
- get api("/user/keys/#{key.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response["title"]).to eq(key.title)
end
it 'exposes SSH key comment as a simple identifier of username + hostname' do
- get api("/user/keys/#{key.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['key']).to include("#{key.user_name} (#{Gitlab.config.gitlab.host})")
@@ -2881,19 +3008,18 @@ RSpec.describe API::Users, feature_category: :user_profile do
user.keys << key
admin
- get api("/user/keys/#{key.id}", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Key Not Found')
end
it "returns 404 for invalid ID" do
- get api("/users/keys/ASDF", admin)
+ get api("/users/keys/ASDF", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
context "scopes" do
- let(:path) { "/user/keys/#{key.id}" }
let(:api_call) { method(:api) }
include_examples 'allows the "read_user" scope'
@@ -2901,11 +3027,13 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe "POST /user/keys" do
- it "creates ssh key", :aggregate_failures do
+ let(:path) { "/user/keys" }
+
+ it "creates ssh key" do
key_attrs = attributes_for(:key, usage_type: :signing)
expect do
- post api("/user/keys", user), params: key_attrs
+ post api(path, user), params: key_attrs
end.to change { user.keys.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -2920,19 +3048,19 @@ RSpec.describe API::Users, feature_category: :user_profile do
optional_attributes = { expires_at: 3.weeks.from_now }
attributes = attributes_for(:key).merge(optional_attributes)
- post api("/user/keys", user), params: attributes
+ post api(path, user), params: attributes
expect(response).to have_gitlab_http_status(:created)
expect(json_response['expires_at'].to_date).to eq(optional_attributes[:expires_at].to_date)
end
it "returns a 401 error if unauthorized" do
- post api("/user/keys"), params: { title: 'some title', key: 'some key' }
+ post api(path), params: { title: 'some title', key: 'some key' }
expect(response).to have_gitlab_http_status(:unauthorized)
end
it "does not create ssh key without key" do
- post api("/user/keys", user), params: { title: 'title' }
+ post api(path, user), params: { title: 'title' }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('key is missing')
@@ -2946,24 +3074,26 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "does not create ssh key without title" do
- post api("/user/keys", user), params: { key: "somekey" }
+ post api(path, user), params: { key: "somekey" }
expect(response).to have_gitlab_http_status(:bad_request)
end
end
describe "DELETE /user/keys/:key_id" do
+ let(:path) { "/user/keys/#{key.id}" }
+
it "deletes existed key" do
user.keys << key
expect do
- delete api("/user/keys/#{key.id}", user)
+ delete api(path, user)
expect(response).to have_gitlab_http_status(:no_content)
end.to change { user.keys.count }.by(-1)
end
it_behaves_like '412 response' do
- let(:request) { api("/user/keys/#{key.id}", user) }
+ let(:request) { api(path, user) }
end
it "returns 404 if key ID not found" do
@@ -2976,21 +3106,23 @@ RSpec.describe API::Users, feature_category: :user_profile do
it "returns 401 error if unauthorized" do
user.keys << key
- delete api("/user/keys/#{key.id}")
+ delete api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
it "returns a 404 for invalid ID" do
- delete api("/users/keys/ASDF", admin)
+ delete api("/users/keys/ASDF", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe 'GET /user/gpg_keys' do
+ let(:path) { '/user/gpg_keys' }
+
context 'when unauthenticated' do
it 'returns authentication error' do
- get api('/user/gpg_keys')
+ get api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -3000,7 +3132,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns array of GPG keys' do
user.gpg_keys << gpg_key
- get api('/user/gpg_keys', user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -3009,7 +3141,6 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
context 'scopes' do
- let(:path) { '/user/gpg_keys' }
let(:api_call) { method(:api) }
include_examples 'allows the "read_user" scope'
@@ -3018,10 +3149,12 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe 'GET /user/gpg_keys/:key_id' do
+ let(:path) { "/user/gpg_keys/#{gpg_key.id}" }
+
it 'returns a single key' do
user.gpg_keys << gpg_key
- get api("/user/gpg_keys/#{gpg_key.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['key']).to eq(gpg_key.key)
@@ -3037,20 +3170,19 @@ RSpec.describe API::Users, feature_category: :user_profile do
it "returns 404 error if admin accesses user's GPG key" do
user.gpg_keys << gpg_key
- get api("/user/gpg_keys/#{gpg_key.id}", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 GPG Key Not Found')
end
it 'returns 404 for invalid ID' do
- get api('/users/gpg_keys/ASDF', admin)
+ get api('/users/gpg_keys/ASDF', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
context 'scopes' do
- let(:path) { "/user/gpg_keys/#{gpg_key.id}" }
let(:api_call) { method(:api) }
include_examples 'allows the "read_user" scope'
@@ -3058,24 +3190,26 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe 'POST /user/gpg_keys' do
+ let(:path) { '/user/gpg_keys' }
+
it 'creates a GPG key' do
key_attrs = attributes_for :gpg_key, key: GpgHelpers::User2.public_key
expect do
- post api('/user/gpg_keys', user), params: key_attrs
+ post api(path, user), params: key_attrs
expect(response).to have_gitlab_http_status(:created)
end.to change { user.gpg_keys.count }.by(1)
end
it 'returns a 401 error if unauthorized' do
- post api('/user/gpg_keys'), params: { key: 'some key' }
+ post api(path), params: { key: 'some key' }
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'does not create GPG key without key' do
- post api('/user/gpg_keys', user)
+ post api(path, user)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('key is missing')
@@ -3109,18 +3243,20 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'returns a 404 for invalid ID' do
- post api('/users/gpg_keys/ASDF/revoke', admin)
+ post api('/users/gpg_keys/ASDF/revoke', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe 'DELETE /user/gpg_keys/:key_id' do
+ let(:path) { "/user/gpg_keys/#{gpg_key.id}" }
+
it 'deletes existing GPG key' do
user.gpg_keys << gpg_key
expect do
- delete api("/user/gpg_keys/#{gpg_key.id}", user)
+ delete api(path, user)
expect(response).to have_gitlab_http_status(:no_content)
end.to change { user.gpg_keys.count }.by(-1)
@@ -3136,22 +3272,24 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'returns 401 error if unauthorized' do
user.gpg_keys << gpg_key
- delete api("/user/gpg_keys/#{gpg_key.id}")
+ delete api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'returns a 404 for invalid ID' do
- delete api('/users/gpg_keys/ASDF', admin)
+ delete api('/users/gpg_keys/ASDF', admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe "GET /user/emails" do
+ let(:path) { '/user/emails' }
+
context "when unauthenticated" do
it "returns authentication error" do
- get api("/user/emails")
+ get api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
@@ -3160,7 +3298,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
it "returns array of emails" do
user.emails << email
- get api("/user/emails", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -3170,7 +3308,6 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
context "scopes" do
- let(:path) { "/user/emails" }
let(:api_call) { method(:api) }
include_examples 'allows the "read_user" scope'
@@ -3179,10 +3316,12 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe "GET /user/emails/:email_id" do
+ let(:path) { "/user/emails/#{email.id}" }
+
it "returns single email" do
user.emails << email
- get api("/user/emails/#{email.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response["email"]).to eq(email.email)
end
@@ -3197,19 +3336,18 @@ RSpec.describe API::Users, feature_category: :user_profile do
user.emails << email
admin
- get api("/user/emails/#{email.id}", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Email Not Found')
end
it "returns 404 for invalid ID" do
- get api("/users/emails/ASDF", admin)
+ get api("/users/emails/ASDF", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
context "scopes" do
- let(:path) { "/user/emails/#{email.id}" }
let(:api_call) { method(:api) }
include_examples 'allows the "read_user" scope'
@@ -3217,21 +3355,23 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe "POST /user/emails" do
+ let(:path) { '/user/emails' }
+
it "creates email" do
email_attrs = attributes_for :email
expect do
- post api("/user/emails", user), params: email_attrs
+ post api(path, user), params: email_attrs
end.to change { user.emails.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
end
it "returns a 401 error if unauthorized" do
- post api("/user/emails"), params: { email: 'some email' }
+ post api(path), params: { email: 'some email' }
expect(response).to have_gitlab_http_status(:unauthorized)
end
it "does not create email with invalid email" do
- post api("/user/emails", user), params: {}
+ post api(path, user), params: {}
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('email is missing')
@@ -3239,18 +3379,20 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe "DELETE /user/emails/:email_id" do
+ let(:path) { "/user/emails/#{email.id}" }
+
it "deletes existed email" do
user.emails << email
expect do
- delete api("/user/emails/#{email.id}", user)
+ delete api(path, user)
expect(response).to have_gitlab_http_status(:no_content)
end.to change { user.emails.count }.by(-1)
end
it_behaves_like '412 response' do
- let(:request) { api("/user/emails/#{email.id}", user) }
+ let(:request) { api(path, user) }
end
it "returns 404 if email ID not found" do
@@ -3263,12 +3405,12 @@ RSpec.describe API::Users, feature_category: :user_profile do
it "returns 401 error if unauthorized" do
user.emails << email
- delete api("/user/emails/#{email.id}")
+ delete api(path)
expect(response).to have_gitlab_http_status(:unauthorized)
end
it "returns 400 for invalid ID" do
- delete api("/user/emails/ASDF", admin)
+ delete api("/user/emails/ASDF", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -3283,12 +3425,18 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe 'POST /users/:id/activate' do
- subject(:activate) { post api("/users/#{user_id}/activate", api_user) }
+ subject(:activate) { post api(path, api_user, **params) }
let(:user_id) { user.id }
+ let(:path) { "/users/#{user_id}/activate" }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { {} }
+ end
context 'performed by a non-admin user' do
let(:api_user) { user }
+ let(:params) { { admin_mode: false } }
it 'is not authorized to perform the action' do
activate
@@ -3299,6 +3447,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'performed by an admin user' do
let(:api_user) { admin }
+ let(:params) { { admin_mode: true } }
context 'for a deactivated user' do
let(:user_id) { deactivated_user.id }
@@ -3351,7 +3500,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
context 'for a user that does not exist' do
- let(:user_id) { 0 }
+ let(:user_id) { non_existing_record_id }
before do
activate
@@ -3363,12 +3512,18 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe 'POST /users/:id/deactivate' do
- subject(:deactivate) { post api("/users/#{user_id}/deactivate", api_user) }
+ subject(:deactivate) { post api(path, api_user, **params) }
let(:user_id) { user.id }
+ let(:path) { "/users/#{user_id}/deactivate" }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { {} }
+ end
context 'performed by a non-admin user' do
let(:api_user) { user }
+ let(:params) { { admin_mode: false } }
it 'is not authorized to perform the action' do
deactivate
@@ -3379,6 +3534,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'performed by an admin user' do
let(:api_user) { admin }
+ let(:params) { { admin_mode: true } }
context 'for an active user' do
let(:activity) { {} }
@@ -3402,7 +3558,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
deactivate
expect(response).to have_gitlab_http_status(:forbidden)
- expect(json_response['message']).to eq("403 Forbidden - The user you are trying to deactivate has been active in the past #{Gitlab::CurrentSettings.deactivate_dormant_users_period} days and cannot be deactivated")
+ expect(json_response['message']).to eq("The user you are trying to deactivate has been active in the past #{Gitlab::CurrentSettings.deactivate_dormant_users_period} days and cannot be deactivated")
expect(user.reload.state).to eq('active')
end
end
@@ -3426,7 +3582,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
deactivate
expect(response).to have_gitlab_http_status(:forbidden)
- expect(json_response['message']).to eq('403 Forbidden - A blocked user cannot be deactivated by the API')
+ expect(json_response['message']).to eq('Error occurred. A blocked user cannot be deactivated')
expect(blocked_user.reload.state).to eq('blocked')
end
end
@@ -3440,7 +3596,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
deactivate
expect(response).to have_gitlab_http_status(:forbidden)
- expect(json_response['message']).to eq('403 Forbidden - A blocked user cannot be deactivated by the API')
+ expect(json_response['message']).to eq('Error occurred. A blocked user cannot be deactivated')
expect(user.reload.state).to eq('ldap_blocked')
end
end
@@ -3452,12 +3608,12 @@ RSpec.describe API::Users, feature_category: :user_profile do
deactivate
expect(response).to have_gitlab_http_status(:forbidden)
- expect(json_response['message']).to eq('403 Forbidden - An internal user cannot be deactivated by the API')
+ expect(json_response['message']).to eq('Internal users cannot be deactivated')
end
end
context 'for a user that does not exist' do
- let(:user_id) { 0 }
+ let(:user_id) { non_existing_record_id }
before do
deactivate
@@ -3480,11 +3636,19 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe 'POST /users/:id/approve' do
- subject(:approve) { post api("/users/#{user_id}/approve", api_user) }
+ subject(:approve) { post api(path, api_user, **params) }
+
+ let(:path) { "/users/#{user_id}/approve" }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:user_id) { pending_user.id }
+ let(:params) { {} }
+ end
context 'performed by a non-admin user' do
let(:api_user) { user }
let(:user_id) { pending_user.id }
+ let(:params) { { admin_mode: false } }
it 'is not authorized to perform the action' do
expect { approve }.not_to change { pending_user.reload.state }
@@ -3495,6 +3659,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'performed by an admin user' do
let(:api_user) { admin }
+ let(:params) { { admin_mode: true } }
context 'for a deactivated user' do
let(:user_id) { deactivated_user.id }
@@ -3558,8 +3723,16 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
end
- describe 'POST /users/:id/reject', :aggregate_failures do
- subject(:reject) { post api("/users/#{user_id}/reject", api_user) }
+ describe 'POST /users/:id/reject' do
+ subject(:reject) { post api(path, api_user, **params) }
+
+ let(:path) { "/users/#{user_id}/reject" }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:user_id) { pending_user.id }
+ let(:params) { {} }
+ let(:success_status_code) { :success }
+ end
shared_examples 'returns 409' do
it 'returns 409' do
@@ -3573,6 +3746,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'performed by a non-admin user' do
let(:api_user) { user }
let(:user_id) { pending_user.id }
+ let(:params) { { admin_mode: false } }
it 'returns 403' do
expect { reject }.not_to change { pending_user.reload.state }
@@ -3583,6 +3757,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'performed by an admin user' do
let(:api_user) { admin }
+ let(:params) { { admin_mode: true } }
context 'for an pending approval user' do
let(:user_id) { pending_user.id }
@@ -3648,13 +3823,21 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
end
- describe 'POST /users/:id/block', :aggregate_failures do
+ describe 'POST /users/:id/block' do
+ subject(:block_user) { post api(path, api_user, **params) }
+
+ let(:user_id) { user.id }
+ let(:path) { "/users/#{user_id}/block" }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { {} }
+ end
+
context 'when admin' do
- subject(:block_user) { post api("/users/#{user_id}/block", admin) }
+ let(:api_user) { admin }
+ let(:params) { { admin_mode: true } }
context 'with an existing user' do
- let(:user_id) { user.id }
-
it 'blocks existing user' do
block_user
@@ -3730,21 +3913,34 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
end
- it 'is not available for non admin users' do
- post api("/users/#{user.id}/block", user)
+ context 'performed by a non-admin user' do
+ let(:api_user) { user }
+ let(:params) { { admin_mode: false } }
- expect(response).to have_gitlab_http_status(:forbidden)
- expect(user.reload.state).to eq('active')
+ it 'returns 403' do
+ block_user
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(user.reload.state).to eq('active')
+ end
end
end
- describe 'POST /users/:id/unblock', :aggregate_failures do
+ describe 'POST /users/:id/unblock' do
+ subject(:unblock_user) { post api(path, api_user, **params) }
+
+ let(:path) { "/users/#{user_id}/unblock" }
+ let(:user_id) { user.id }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { {} }
+ end
+
context 'when admin' do
- subject(:unblock_user) { post api("/users/#{user_id}/unblock", admin) }
+ let(:api_user) { admin }
+ let(:params) { { admin_mode: true } }
context 'with an existing user' do
- let(:user_id) { user.id }
-
it 'unblocks existing user' do
unblock_user
@@ -3817,20 +4013,34 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
end
- it 'is not available for non admin users' do
- post api("/users/#{user.id}/unblock", user)
- expect(response).to have_gitlab_http_status(:forbidden)
- expect(user.reload.state).to eq('active')
+ context 'performed by a non-admin user' do
+ let(:api_user) { user }
+ let(:params) { { admin_mode: false } }
+
+ it 'returns 403' do
+ unblock_user
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(user.reload.state).to eq('active')
+ end
end
end
- describe 'POST /users/:id/ban', :aggregate_failures do
+ describe 'POST /users/:id/ban' do
+ subject(:ban_user) { post api(path, api_user, **params) }
+
+ let(:path) { "/users/#{user_id}/ban" }
+ let(:user_id) { user.id }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:params) { {} }
+ end
+
context 'when admin' do
- subject(:ban_user) { post api("/users/#{user_id}/ban", admin) }
+ let(:api_user) { admin }
+ let(:params) { { admin_mode: true } }
context 'with an active user' do
- let(:user_id) { user.id }
-
it 'bans an active user' do
ban_user
@@ -3898,17 +4108,32 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
end
- it 'is not available for non-admin users' do
- post api("/users/#{user.id}/ban", user)
+ context 'performed by a non-admin user' do
+ let(:api_user) { user }
+ let(:params) { { admin_mode: false } }
- expect(response).to have_gitlab_http_status(:forbidden)
- expect(user.reload.state).to eq('active')
+ it 'returns 403' do
+ ban_user
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(user.reload.state).to eq('active')
+ end
end
end
- describe 'POST /users/:id/unban', :aggregate_failures do
+ describe 'POST /users/:id/unban' do
+ subject(:unban_user) { post api(path, api_user, **params) }
+
+ let(:path) { "/users/#{user_id}/unban" }
+
+ it_behaves_like 'POST request permissions for admin mode' do
+ let(:user_id) { banned_user.id }
+ let(:params) { {} }
+ end
+
context 'when admin' do
- subject(:unban_user) { post api("/users/#{user_id}/unban", admin) }
+ let(:api_user) { admin }
+ let(:params) { { admin_mode: true } }
context 'with a banned user' do
let(:user_id) { banned_user.id }
@@ -3979,37 +4204,42 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
end
- it 'is not available for non admin users' do
- post api("/users/#{banned_user.id}/unban", user)
+ context 'performed by a non-admin user' do
+ let(:api_user) { user }
+ let(:params) { { admin_mode: false } }
+ let(:user_id) { banned_user.id }
- expect(response).to have_gitlab_http_status(:forbidden)
- expect(user.reload.state).to eq('active')
+ it 'returns 403' do
+ unban_user
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(user.reload.state).to eq('active')
+ end
end
end
describe "GET /users/:id/memberships" do
+ subject(:request) { get api(path, requesting_user, admin_mode: true) }
+
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) }
let(:requesting_user) { create(:user) }
+ let(:path) { "/users/#{user.id}/memberships" }
before_all do
project.add_guest(user)
group.add_guest(user)
end
- it "responses with 403" do
- get api("/users/#{user.id}/memberships", requesting_user)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ it_behaves_like 'GET request permissions for admin mode'
context 'requested by admin user' do
let(:requesting_user) { create(:user, :admin) }
it "responses successfully" do
- get api("/users/#{user.id}/memberships", requesting_user)
+ request
aggregate_failures 'expect successful response including groups and projects' do
expect(response).to have_gitlab_http_status(:ok)
@@ -4024,22 +4254,23 @@ RSpec.describe API::Users, feature_category: :user_profile do
it 'does not submit N+1 DB queries' do
# Avoid setup queries
- get api("/users/#{user.id}/memberships", requesting_user)
+ request
+ expect(response).to have_gitlab_http_status(:ok)
control = ActiveRecord::QueryRecorder.new do
- get api("/users/#{user.id}/memberships", requesting_user)
+ request
end
create_list(:project, 5).map { |project| project.add_guest(user) }
expect do
- get api("/users/#{user.id}/memberships", requesting_user)
+ request
end.not_to exceed_query_limit(control)
end
context 'with type filter' do
it "only returns project memberships" do
- get api("/users/#{user.id}/memberships?type=Project", requesting_user)
+ get api("/users/#{user.id}/memberships?type=Project", requesting_user, admin_mode: true)
aggregate_failures do
expect(json_response).to contain_exactly(a_hash_including('source_type' => 'Project'))
@@ -4048,7 +4279,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "only returns group memberships" do
- get api("/users/#{user.id}/memberships?type=Namespace", requesting_user)
+ get api("/users/#{user.id}/memberships?type=Namespace", requesting_user, admin_mode: true)
aggregate_failures do
expect(json_response).to contain_exactly(a_hash_including('source_type' => 'Namespace'))
@@ -4057,7 +4288,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it "recognizes unsupported types" do
- get api("/users/#{user.id}/memberships?type=foo", requesting_user)
+ get api("/users/#{user.id}/memberships?type=foo", requesting_user, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -4068,10 +4299,13 @@ RSpec.describe API::Users, feature_category: :user_profile do
context "user activities", :clean_gitlab_redis_shared_state do
let_it_be(:old_active_user) { create(:user, last_activity_on: Time.utc(2000, 1, 1)) }
let_it_be(:newly_active_user) { create(:user, last_activity_on: 2.days.ago.midday) }
+ let(:path) { '/user/activities' }
+
+ it_behaves_like 'GET request permissions for admin mode'
context 'last activity as normal user' do
it 'has no permission' do
- get api("/user/activities", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -4079,7 +4313,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'as admin' do
it 'returns the activities from the last 6 months' do
- get api("/user/activities", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to include_pagination_headers
expect(json_response.size).to eq(1)
@@ -4093,7 +4327,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'passing a :from parameter' do
it 'returns the activities from the given date' do
- get api("/user/activities?from=2000-1-1", admin)
+ get api("#{path}?from=2000-1-1", admin, admin_mode: true)
expect(response).to include_pagination_headers
expect(json_response.size).to eq(2)
@@ -4113,6 +4347,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
let(:user_with_status) { user_status.user }
let(:params) { {} }
let(:request_user) { user }
+ let(:path) { '/user/status' }
shared_examples '/user/status successful response' do
context 'when request is successful' do
@@ -4150,7 +4385,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
set_user_status
expect(response).to have_gitlab_http_status(:success)
- expect(user_with_status.status).to be_nil
+ expect(user_with_status.reset.status).to be_nil
end
end
end
@@ -4178,7 +4413,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
set_user_status
expect(response).to have_gitlab_http_status(:success)
- expect(user_with_status.status.clear_status_at).to be_nil
+ expect(user_with_status.reset.status.clear_status_at).to be_nil
end
end
@@ -4194,13 +4429,11 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
describe 'GET' do
- let(:path) { '/user/status' }
-
it_behaves_like 'rendering user status'
end
describe 'PUT' do
- subject(:set_user_status) { put api('/user/status', request_user), params: params }
+ subject(:set_user_status) { put api(path, request_user), params: params }
include_examples '/user/status successful response'
@@ -4217,7 +4450,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
set_user_status
expect(response).to have_gitlab_http_status(:success)
- expect(user_with_status.status).to be_nil
+ expect(user_with_status.reset.status).to be_nil
end
end
@@ -4229,13 +4462,13 @@ RSpec.describe API::Users, feature_category: :user_profile do
set_user_status
expect(response).to have_gitlab_http_status(:success)
- expect(user_with_status.status.clear_status_at).to be_nil
+ expect(user_with_status.reset.status.clear_status_at).to be_nil
end
end
end
describe 'PATCH' do
- subject(:set_user_status) { patch api('/user/status', request_user), params: params }
+ subject(:set_user_status) { patch api(path, request_user), params: params }
include_examples '/user/status successful response'
@@ -4274,57 +4507,41 @@ RSpec.describe API::Users, feature_category: :user_profile do
let(:name) { 'new pat' }
let(:expires_at) { 3.days.from_now.to_date.to_s }
let(:scopes) { %w(api read_user) }
+ let(:path) { "/users/#{user.id}/personal_access_tokens" }
+ let(:params) { { name: name, scopes: scopes, expires_at: expires_at } }
+
+ it_behaves_like 'POST request permissions for admin mode'
it 'returns error if required attributes are missing' do
- post api("/users/#{user.id}/personal_access_tokens", admin)
+ post api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('name is missing, scopes is missing, scopes does not have a valid value')
end
it 'returns a 404 error if user not found' do
- post api("/users/#{non_existing_record_id}/personal_access_tokens", admin),
- params: {
- name: name,
- scopes: scopes,
- expires_at: expires_at
- }
+ post api("/users/#{non_existing_record_id}/personal_access_tokens", admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns a 401 error when not authenticated' do
- post api("/users/#{user.id}/personal_access_tokens"),
- params: {
- name: name,
- scopes: scopes,
- expires_at: expires_at
- }
+ post api(path), params: params
expect(response).to have_gitlab_http_status(:unauthorized)
expect(json_response['message']).to eq('401 Unauthorized')
end
it 'returns a 403 error when authenticated as normal user' do
- post api("/users/#{user.id}/personal_access_tokens", user),
- params: {
- name: name,
- scopes: scopes,
- expires_at: expires_at
- }
+ post api(path, user), params: params
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['message']).to eq('403 Forbidden')
end
it 'creates a personal access token when authenticated as admin' do
- post api("/users/#{user.id}/personal_access_tokens", admin),
- params: {
- name: name,
- expires_at: expires_at,
- scopes: scopes
- }
+ post api(path, admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq(name)
@@ -4338,7 +4555,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
context 'when an error is thrown by the model' do
- let!(:admin_personal_access_token) { create(:personal_access_token, user: admin) }
+ let!(:admin_personal_access_token) { create(:personal_access_token, :admin_mode, user: admin) }
let(:error_message) { 'error message' }
before do
@@ -4351,12 +4568,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'returns the error' do
- post api("/users/#{user.id}/personal_access_tokens", personal_access_token: admin_personal_access_token),
- params: {
- name: name,
- expires_at: expires_at,
- scopes: scopes
- }
+ post api(path, personal_access_token: admin_personal_access_token), params: params
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['message']).to eq(error_message)
@@ -4370,9 +4582,12 @@ RSpec.describe API::Users, feature_category: :user_profile do
let_it_be(:expired_personal_access_token) { create(:personal_access_token, :expired, user: user) }
let_it_be(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
let_it_be(:revoked_impersonation_token) { create(:personal_access_token, :impersonation, :revoked, user: user) }
+ let(:path) { "/users/#{user.id}/impersonation_tokens" }
+
+ it_behaves_like 'GET request permissions for admin mode'
it 'returns a 404 error if user not found' do
- get api("/users/#{non_existing_record_id}/impersonation_tokens", admin)
+ get api("/users/#{non_existing_record_id}/impersonation_tokens", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
@@ -4386,7 +4601,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'returns an array of all impersonated tokens' do
- get api("/users/#{user.id}/impersonation_tokens", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -4395,7 +4610,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'returns an array of active impersonation tokens if state active' do
- get api("/users/#{user.id}/impersonation_tokens?state=active", admin)
+ get api("#{path}?state=active", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -4405,7 +4620,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'returns an array of inactive personal access tokens if active is set to false' do
- get api("/users/#{user.id}/impersonation_tokens?state=inactive", admin)
+ get api("#{path}?state=inactive", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
@@ -4419,16 +4634,20 @@ RSpec.describe API::Users, feature_category: :user_profile do
let(:expires_at) { '2016-12-28' }
let(:scopes) { %w(api read_user) }
let(:impersonation) { true }
+ let(:path) { "/users/#{user.id}/impersonation_tokens" }
+ let(:params) { { name: name, expires_at: expires_at, scopes: scopes, impersonation: impersonation } }
+
+ it_behaves_like 'POST request permissions for admin mode'
it 'returns validation error if impersonation token misses some attributes' do
- post api("/users/#{user.id}/impersonation_tokens", admin)
+ post api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('name is missing')
end
it 'returns a 404 error if user not found' do
- post api("/users/#{non_existing_record_id}/impersonation_tokens", admin),
+ post api("/users/#{non_existing_record_id}/impersonation_tokens", admin, admin_mode: true),
params: {
name: name,
expires_at: expires_at
@@ -4439,7 +4658,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'returns a 403 error when authenticated as normal user' do
- post api("/users/#{user.id}/impersonation_tokens", user),
+ post api(path, user),
params: {
name: name,
expires_at: expires_at
@@ -4450,13 +4669,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
end
it 'creates a impersonation token' do
- post api("/users/#{user.id}/impersonation_tokens", admin),
- params: {
- name: name,
- expires_at: expires_at,
- scopes: scopes,
- impersonation: impersonation
- }
+ post api(path, admin, admin_mode: true), params: params
expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq(name)
@@ -4474,37 +4687,40 @@ RSpec.describe API::Users, feature_category: :user_profile do
describe 'GET /users/:user_id/impersonation_tokens/:impersonation_token_id' do
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
+ let(:path) { "/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}" }
+
+ it_behaves_like 'GET request permissions for admin mode'
it 'returns 404 error if user not found' do
- get api("/users/#{non_existing_record_id}/impersonation_tokens/1", admin)
+ get api("/users/#{non_existing_record_id}/impersonation_tokens/1", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns a 404 error if impersonation token not found' do
- get api("/users/#{user.id}/impersonation_tokens/#{non_existing_record_id}", admin)
+ get api("/users/#{user.id}/impersonation_tokens/#{non_existing_record_id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Impersonation Token Not Found')
end
it 'returns a 404 error if token is not impersonation token' do
- get api("/users/#{user.id}/impersonation_tokens/#{personal_access_token.id}", admin)
+ get api("/users/#{user.id}/impersonation_tokens/#{personal_access_token.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Impersonation Token Not Found')
end
it 'returns a 403 error when authenticated as normal user' do
- get api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['message']).to eq('403 Forbidden')
end
it 'returns an impersonation token' do
- get api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['token']).not_to be_present
@@ -4515,41 +4731,44 @@ RSpec.describe API::Users, feature_category: :user_profile do
describe 'DELETE /users/:user_id/impersonation_tokens/:impersonation_token_id' do
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:impersonation_token) { create(:personal_access_token, :impersonation, user: user) }
+ let(:path) { "/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}" }
+
+ it_behaves_like 'DELETE request permissions for admin mode'
it 'returns a 404 error if user not found' do
- delete api("/users/#{non_existing_record_id}/impersonation_tokens/1", admin)
+ delete api("/users/#{non_existing_record_id}/impersonation_tokens/1", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 User Not Found')
end
it 'returns a 404 error if impersonation token not found' do
- delete api("/users/#{user.id}/impersonation_tokens/#{non_existing_record_id}", admin)
+ delete api("/users/#{user.id}/impersonation_tokens/#{non_existing_record_id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Impersonation Token Not Found')
end
it 'returns a 404 error if token is not impersonation token' do
- delete api("/users/#{user.id}/impersonation_tokens/#{personal_access_token.id}", admin)
+ delete api("/users/#{user.id}/impersonation_tokens/#{personal_access_token.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Impersonation Token Not Found')
end
it 'returns a 403 error when authenticated as normal user' do
- delete api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", user)
+ delete api(path, user)
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['message']).to eq('403 Forbidden')
end
it_behaves_like '412 response' do
- let(:request) { api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin) }
+ let(:request) { api(path, admin, admin_mode: true) }
end
it 'revokes a impersonation token' do
- delete api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin)
+ delete api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:no_content)
expect(impersonation_token.revoked).to be_falsey
@@ -4560,6 +4779,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
describe 'GET /users/:id/associations_count' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :public, group: group) }
+ let(:path) { "/users/#{user.id}/associations_count" }
let(:associations) do
{
groups_count: 1,
@@ -4576,9 +4796,11 @@ RSpec.describe API::Users, feature_category: :user_profile do
create_list(:issue, 2, project: project, author: user)
end
+ it_behaves_like 'GET request permissions for admin mode'
+
context 'as an unauthorized user' do
it 'returns 401 unauthorized' do
- get api("/users/#{user.id}/associations_count", nil)
+ get api(path, nil)
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -4595,7 +4817,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'with the current user id' do
it 'returns valid JSON response' do
- get api("/users/#{user.id}/associations_count", user)
+ get api(path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_a Hash
@@ -4607,7 +4829,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'as an admin user' do
context 'with invalid user id' do
it 'returns 404 User Not Found' do
- get api("/users/#{non_existing_record_id}/associations_count", admin)
+ get api("/users/#{non_existing_record_id}/associations_count", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -4615,7 +4837,7 @@ RSpec.describe API::Users, feature_category: :user_profile do
context 'with valid user id' do
it 'returns valid JSON response' do
- get api("/users/#{user.id}/associations_count", admin)
+ get api(path, admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_a Hash
@@ -4629,4 +4851,169 @@ RSpec.describe API::Users, feature_category: :user_profile do
let(:attributable) { user }
let(:other_attributable) { admin }
end
+
+ describe 'POST /user/runners', feature_category: :runner_fleet do
+ subject(:request) { post api(path, current_user, **post_args), params: runner_attrs }
+
+ let_it_be(:group_owner) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, namespace: group) }
+
+ let(:post_args) { { admin_mode: true } }
+ let(:runner_attrs) { { runner_type: 'instance_type' } }
+ let(:path) { '/user/runners' }
+
+ before do
+ group.add_owner(group_owner)
+ end
+
+ shared_context 'returns forbidden when user does not have sufficient permissions' do
+ let(:current_user) { admin }
+ let(:post_args) { { admin_mode: false } }
+
+ it 'does not create a runner' do
+ expect do
+ request
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end.not_to change { Ci::Runner.count }
+ end
+ end
+
+ shared_examples 'creates a runner' do
+ it 'creates a runner' do
+ expect do
+ request
+
+ expect(response).to have_gitlab_http_status(:created)
+ end.to change { Ci::Runner.count }.by(1)
+ end
+ end
+
+ shared_examples 'fails to create runner with :bad_request' do
+ it 'does not create runner' do
+ expect do
+ request
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to include(expected_error)
+ end.not_to change { Ci::Runner.count }
+ end
+ end
+
+ context 'when runner_type is :instance_type' do
+ let(:runner_attrs) { { runner_type: 'instance_type' } }
+
+ context 'when user has sufficient permissions' do
+ let(:current_user) { admin }
+
+ it_behaves_like 'creates a runner'
+ end
+
+ it_behaves_like 'returns forbidden when user does not have sufficient permissions'
+
+ context 'when model validation fails' do
+ let(:runner_attrs) { { runner_type: 'instance_type', run_untagged: false, tag_list: [] } }
+ let(:current_user) { admin }
+
+ it_behaves_like 'fails to create runner with :bad_request' do
+ let(:expected_error) { 'Tags list can not be empty' }
+ end
+ end
+ end
+
+ context 'when runner_type is :group_type' do
+ let(:post_args) { {} }
+
+ context 'when group_id is specified' do
+ let(:runner_attrs) { { runner_type: 'group_type', group_id: group.id } }
+
+ context 'when user has sufficient permissions' do
+ let(:current_user) { group_owner }
+
+ it_behaves_like 'creates a runner'
+ end
+
+ it_behaves_like 'returns forbidden when user does not have sufficient permissions'
+ end
+
+ context 'when group_id is not specified' do
+ let(:runner_attrs) { { runner_type: 'group_type' } }
+ let(:current_user) { group_owner }
+
+ it 'fails to create runner with :bad_request' do
+ expect do
+ request
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to include('group_id is missing')
+ end.not_to change { Ci::Runner.count }
+ end
+ end
+ end
+
+ context 'when runner_type is :project_type' do
+ let(:post_args) { {} }
+
+ context 'when project_id is specified' do
+ let(:runner_attrs) { { runner_type: 'project_type', project_id: project.id } }
+
+ context 'when user has sufficient permissions' do
+ let(:current_user) { group_owner }
+
+ it_behaves_like 'creates a runner'
+ end
+
+ it_behaves_like 'returns forbidden when user does not have sufficient permissions'
+ end
+
+ context 'when project_id is not specified' do
+ let(:runner_attrs) { { runner_type: 'project_type' } }
+ let(:current_user) { group_owner }
+
+ it 'fails to create runner with :bad_request' do
+ expect do
+ request
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to include('project_id is missing')
+ end.not_to change { Ci::Runner.count }
+ end
+ end
+ end
+
+ context 'with missing runner_type' do
+ let(:runner_attrs) { {} }
+ let(:current_user) { admin }
+
+ it 'fails to create runner with :bad_request' do
+ expect do
+ request
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('runner_type is missing, runner_type does not have a valid value')
+ end.not_to change { Ci::Runner.count }
+ end
+ end
+
+ context 'with unknown runner_type' do
+ let(:runner_attrs) { { runner_type: 'unknown' } }
+ let(:current_user) { admin }
+
+ it 'fails to create runner with :bad_request' do
+ expect do
+ request
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('runner_type does not have a valid value')
+ end.not_to change { Ci::Runner.count }
+ end
+ end
+
+ it 'returns a 401 error if unauthorized' do
+ post api(path), params: runner_attrs
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
end
diff --git a/spec/requests/api/v3/github_spec.rb b/spec/requests/api/v3/github_spec.rb
index 0b8fac5c55c..b6fccd9b7cb 100644
--- a/spec/requests/api/v3/github_spec.rb
+++ b/spec/requests/api/v3/github_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe API::V3::Github, feature_category: :integrations do
+RSpec.describe API::V3::Github, :aggregate_failures, feature_category: :integrations do
let_it_be(:user) { create(:user) }
let_it_be(:unauthorized_user) { create(:user) }
let_it_be(:admin) { create(:user, :admin) }
@@ -13,6 +13,13 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
end
describe 'GET /orgs/:namespace/repos' do
+ it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
+ subject do
+ group = create(:group)
+ jira_get v3_api("/orgs/#{group.path}/repos", user)
+ end
+ end
+
it 'returns an empty array' do
group = create(:group)
@@ -32,6 +39,10 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
end
describe 'GET /user/repos' do
+ it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
+ subject { jira_get v3_api('/user/repos', user) }
+ end
+
it 'returns an empty array' do
jira_get v3_api('/user/repos', user)
@@ -117,6 +128,10 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
describe 'GET /users/:username' do
let!(:user1) { create(:user, username: 'jane.porter') }
+ it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
+ subject { jira_get v3_api("/users/#{user.username}", user) }
+ end
+
context 'user exists' do
it 'responds with the expected user' do
jira_get v3_api("/users/#{user.username}", user)
@@ -155,6 +170,10 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
let(:project) { create(:project, :empty_repo, path: 'project.with.dot', group: group) }
let(:events_path) { "/repos/#{group.path}/#{project.path}/events" }
+ it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
+ subject { jira_get v3_api(events_path, user) }
+ end
+
context 'if there are no merge requests' do
it 'returns an empty array' do
jira_get v3_api(events_path, user)
@@ -232,6 +251,10 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
describe 'GET /-/jira/pulls' do
let(:route) { '/repos/-/jira/pulls' }
+ it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
+ subject { perform_request }
+ end
+
it 'returns an array of merge requests with github format' do
perform_request
@@ -258,6 +281,10 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
describe 'GET /repos/:namespace/:project/pulls' do
let(:route) { "/repos/#{project.namespace.path}/#{project.path}/pulls" }
+ it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
+ subject { perform_request }
+ end
+
it 'returns an array of merge requests for the proper project in github format' do
perform_request
@@ -279,6 +306,10 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
end
describe 'GET /repos/:namespace/:project/pulls/:id' do
+ it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
+ subject { jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", user) }
+ end
+
context 'when user has access to the merge requests' do
it 'returns the requested merge request in github format' do
jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", user)
@@ -300,7 +331,7 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
context 'when instance admin' do
it 'returns the requested merge request in github format' do
- jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", admin)
+ jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/pulls/#{merge_request.id}", admin, admin_mode: true)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('entities/github/pull_request')
@@ -312,8 +343,8 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
describe 'GET /users/:namespace/repos' do
let(:group) { create(:group, name: 'foo') }
- def expect_project_under_namespace(projects, namespace, user)
- jira_get v3_api("/users/#{namespace.path}/repos", user)
+ def expect_project_under_namespace(projects, namespace, user, admin_mode = false)
+ jira_get v3_api("/users/#{namespace.path}/repos", user, admin_mode: admin_mode)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
@@ -331,6 +362,10 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
expect(json_response.size).to eq(projects.size)
end
+ it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
+ subject { jira_get v3_api("/users/#{user.namespace.path}/repos", user) }
+ end
+
context 'group namespace' do
let(:project) { create(:project, group: group) }
let!(:project2) { create(:project, :public, group: group) }
@@ -343,7 +378,7 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
let(:user) { create(:user, :admin) }
it 'returns an array of projects belonging to group' do
- expect_project_under_namespace([project, project2], group, user)
+ expect_project_under_namespace([project, project2], group, user, true)
end
context 'with a private group' do
@@ -351,7 +386,7 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
let!(:project2) { create(:project, :private, group: group) }
it 'returns an array of projects belonging to group' do
- expect_project_under_namespace([project, project2], group, user)
+ expect_project_under_namespace([project, project2], group, user, true)
end
end
end
@@ -423,6 +458,10 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
describe 'GET /repos/:namespace/:project/branches' do
context 'authenticated' do
+ it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
+ subject { jira_get v3_api("/repos/#{project.namespace.path}/#{project.path}/branches", user) }
+ end
+
context 'updating project feature usage' do
it 'counts Jira Cloud integration as enabled' do
user_agent = 'Jira DVCS Connector Vertigo/4.42.0'
@@ -473,7 +512,7 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
expect(response).to have_gitlab_http_status(:ok)
end
- context 'when the project has no repository', :aggregate_failures do
+ context 'when the project has no repository' do
let_it_be(:project) { create(:project, creator: user) }
it 'returns an empty collection response' do
@@ -516,7 +555,11 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
end
context 'authenticated' do
- it 'returns commit with github format', :aggregate_failures do
+ it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
+ subject { call_api }
+ end
+
+ it 'returns commit with github format' do
call_api
expect(response).to have_gitlab_http_status(:ok)
@@ -552,7 +595,7 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
.and_call_original
end
- it 'handles the error, logs it, and returns empty diff files', :aggregate_failures do
+ it 'handles the error, logs it, and returns empty diff files' do
allow(Gitlab::GitalyClient).to receive(:call)
.with(*commit_diff_args)
.and_raise(GRPC::DeadlineExceeded)
@@ -567,7 +610,7 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
expect(response_diff_files(response)).to be_blank
end
- it 'only calls Gitaly once for all attempts within a period of time', :aggregate_failures do
+ it 'only calls Gitaly once for all attempts within a period of time' do
expect(Gitlab::GitalyClient).to receive(:call)
.with(*commit_diff_args)
.once # <- once
@@ -581,7 +624,7 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
end
end
- it 'calls Gitaly again after a period of time', :aggregate_failures do
+ it 'calls Gitaly again after a period of time' do
expect(Gitlab::GitalyClient).to receive(:call)
.with(*commit_diff_args)
.twice # <- twice
@@ -648,13 +691,14 @@ RSpec.describe API::V3::Github, feature_category: :integrations do
get path, headers: { 'User-Agent' => user_agent }
end
- def v3_api(path, user = nil, personal_access_token: nil, oauth_access_token: nil)
+ def v3_api(path, user = nil, personal_access_token: nil, oauth_access_token: nil, admin_mode: false)
api(
path,
user,
version: 'v3',
personal_access_token: personal_access_token,
- oauth_access_token: oauth_access_token
+ oauth_access_token: oauth_access_token,
+ admin_mode: admin_mode
)
end
end
diff --git a/spec/requests/dashboard_controller_spec.rb b/spec/requests/dashboard_controller_spec.rb
index 1c8ab843ebe..d7f01b8a7ab 100644
--- a/spec/requests/dashboard_controller_spec.rb
+++ b/spec/requests/dashboard_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DashboardController, feature_category: :authentication_and_authorization do
+RSpec.describe DashboardController, feature_category: :system_access do
context 'token authentication' do
it_behaves_like 'authenticates sessionless user for the request spec', 'issues atom', public_resource: false do
let(:url) { issues_dashboard_url(:atom, assignee_username: user.username) }
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 02b99eba8ce..5b50e8a1021 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -230,6 +230,17 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do
context 'when authenticated' do
it 'creates a new project under the existing namespace' do
+ # current scenario does not matter with the user activity case,
+ # so stub/double it to escape more sql running times limit
+ activity_service = instance_double(::Users::ActivityService)
+ allow(::Users::ActivityService).to receive(:new).and_return(activity_service)
+ allow(activity_service).to receive(:execute)
+
+ # During project creation, we need to track the project wiki
+ # repository. So it is over the query limit threshold, and we
+ # have to adjust it.
+ allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(101)
+
expect do
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:ok)
@@ -472,10 +483,11 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do
end
context 'when the request is not from gitlab-workhorse' do
- it 'raises an exception' do
- expect do
- get("/#{project.full_path}.git/info/refs?service=git-upload-pack")
- end.to raise_error(JWT::DecodeError)
+ it 'responds with 403 Forbidden' do
+ get("/#{project.full_path}.git/info/refs?service=git-upload-pack")
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response.body).to eq('Nil JSON web token')
end
end
@@ -1112,10 +1124,11 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do
end
context 'when the request is not from gitlab-workhorse' do
- it 'raises an exception' do
- expect do
- get("/#{project.full_path}.git/info/refs?service=git-upload-pack")
- end.to raise_error(JWT::DecodeError)
+ it 'responds with 403 Forbidden' do
+ get("/#{project.full_path}.git/info/refs?service=git-upload-pack")
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response.body).to eq('Nil JSON web token')
end
end
diff --git a/spec/requests/groups/achievements_controller_spec.rb b/spec/requests/groups/achievements_controller_spec.rb
new file mode 100644
index 00000000000..26ca0039984
--- /dev/null
+++ b/spec/requests/groups/achievements_controller_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::AchievementsController, feature_category: :user_profile do
+ let_it_be(:user) { create(:user) }
+
+ shared_examples 'response with 404 status' do
+ it 'returns 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ shared_examples 'ok response with index template' do
+ it 'renders the index template' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:index)
+ end
+ end
+
+ shared_examples 'ok response with index template if authorized' do
+ context 'with a private group' do
+ let(:group) { create(:group, :private) }
+
+ context 'with authorized user' do
+ before do
+ group.add_guest(user)
+ sign_in(user)
+ end
+
+ it_behaves_like 'ok response with index template'
+
+ context 'when achievements ff is disabled' do
+ before do
+ stub_feature_flags(achievements: false)
+ end
+
+ it_behaves_like 'response with 404 status'
+ end
+ end
+
+ context 'with unauthorized user' do
+ before do
+ sign_in(user)
+ end
+
+ it_behaves_like 'response with 404 status'
+ end
+
+ context 'with anonymous user' do
+ it 'redirects to sign_in page' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context 'with a public group' do
+ let(:group) { create(:group, :public) }
+
+ context 'with anonymous user' do
+ it_behaves_like 'ok response with index template'
+ end
+ end
+ end
+
+ describe 'GET #index' do
+ subject { get group_achievements_path(group) }
+
+ it_behaves_like 'ok response with index template if authorized'
+ end
+end
diff --git a/spec/requests/groups/email_campaigns_controller_spec.rb b/spec/requests/groups/email_campaigns_controller_spec.rb
index 7db5c084793..b6e765eba37 100644
--- a/spec/requests/groups/email_campaigns_controller_spec.rb
+++ b/spec/requests/groups/email_campaigns_controller_spec.rb
@@ -38,11 +38,7 @@ RSpec.describe Groups::EmailCampaignsController, feature_category: :navigation d
expect(subject).to have_gitlab_http_status(:redirect)
end
- context 'on .com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
+ context 'on SaaS', :saas do
it 'emits a snowplow event', :snowplow do
subject
diff --git a/spec/requests/groups/observability_controller_spec.rb b/spec/requests/groups/observability_controller_spec.rb
index 471cad40c90..b82cf2b0bad 100644
--- a/spec/requests/groups/observability_controller_spec.rb
+++ b/spec/requests/groups/observability_controller_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Groups::ObservabilityController, feature_category: :tracing do
end
end
- context 'when user is not a developer' do
+ context 'when user is a guest' do
before do
sign_in(user)
end
@@ -36,10 +36,10 @@ RSpec.describe Groups::ObservabilityController, feature_category: :tracing do
end
end
- context 'when user is authenticated and a developer' do
+ context 'when user has the correct permissions' do
before do
sign_in(user)
- group.add_developer(user)
+ set_permissions
end
context 'when observability url is missing' do
@@ -75,13 +75,21 @@ RSpec.describe Groups::ObservabilityController, feature_category: :tracing do
let(:path) { group_observability_explore_path(group) }
let(:expected_observability_path) { "#{observability_url}/-/#{group.id}/explore" }
- it_behaves_like 'observability route request'
+ it_behaves_like 'observability route request' do
+ let(:set_permissions) do
+ group.add_developer(user)
+ end
+ end
end
describe 'GET #datasources' do
let(:path) { group_observability_datasources_path(group) }
let(:expected_observability_path) { "#{observability_url}/-/#{group.id}/datasources" }
- it_behaves_like 'observability route request'
+ it_behaves_like 'observability route request' do
+ let(:set_permissions) do
+ group.add_maintainer(user)
+ end
+ end
end
end
diff --git a/spec/requests/groups/settings/access_tokens_controller_spec.rb b/spec/requests/groups/settings/access_tokens_controller_spec.rb
index f26b69f8d30..0204af8ea8e 100644
--- a/spec/requests/groups/settings/access_tokens_controller_spec.rb
+++ b/spec/requests/groups/settings/access_tokens_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::Settings::AccessTokensController, feature_category: :authentication_and_authorization do
+RSpec.describe Groups::Settings::AccessTokensController, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:resource) { create(:group) }
let_it_be(:access_token_user) { create(:user, :project_bot) }
diff --git a/spec/requests/groups/settings/applications_controller_spec.rb b/spec/requests/groups/settings/applications_controller_spec.rb
index fb91cd8bdab..2fcf80658b2 100644
--- a/spec/requests/groups/settings/applications_controller_spec.rb
+++ b/spec/requests/groups/settings/applications_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::Settings::ApplicationsController, feature_category: :authentication_and_authorization do
+RSpec.describe Groups::Settings::ApplicationsController, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:application) { create(:oauth_application, owner_id: group.id, owner_type: 'Namespace') }
diff --git a/spec/requests/groups/usage_quotas_controller_spec.rb b/spec/requests/groups/usage_quotas_controller_spec.rb
index a329398aab3..67aef23704a 100644
--- a/spec/requests/groups/usage_quotas_controller_spec.rb
+++ b/spec/requests/groups/usage_quotas_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Groups::UsageQuotasController, :with_license, feature_category: :subscription_cost_management do
+RSpec.describe Groups::UsageQuotasController, :with_license, feature_category: :consumables_cost_management do
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/ide_controller_spec.rb b/spec/requests/ide_controller_spec.rb
index b287ded799d..fe7210e4372 100644
--- a/spec/requests/ide_controller_spec.rb
+++ b/spec/requests/ide_controller_spec.rb
@@ -19,16 +19,15 @@ RSpec.describe IdeController, feature_category: :web_ide do
let_it_be(:top_nav_partial) { 'layouts/header/_default' }
let(:user) { creator }
- let(:branch) { '' }
- def find_csp_frame_src
+ def find_csp_source(key)
csp = response.headers['Content-Security-Policy']
- # Transform "frame-src foo bar; connect-src foo bar; script-src ..."
- # into array of connect-src values
+ # Transform "default-src foo bar; connect-src foo bar; script-src ..."
+ # into array of values for a single directive based on the given key
csp.split(';')
.map(&:strip)
- .find { |entry| entry.starts_with?('frame-src') }
+ .find { |entry| entry.starts_with?(key) }
.split(' ')
.drop(1)
end
@@ -42,14 +41,14 @@ RSpec.describe IdeController, feature_category: :web_ide do
subject { get route }
shared_examples 'user access rights check' do
- context 'user can read project' do
+ context 'when user can read project' do
it 'increases the views counter' do
expect(Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_views_count)
subject
end
- context 'user can read project but cannot push code' do
+ context 'when user can read project but cannot push code' do
include ProjectForksHelper
let(:user) { reporter }
@@ -60,7 +59,15 @@ RSpec.describe IdeController, feature_category: :web_ide do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:project)).to eq project
- expect(assigns(:fork_info)).to eq({ fork_path: controller.helpers.ide_fork_and_edit_path(project, branch, '', with_notice: false) })
+
+ expect(assigns(:fork_info)).to eq({
+ fork_path: controller.helpers.ide_fork_and_edit_path(
+ project,
+ '',
+ '',
+ with_notice: false
+ )
+ })
end
it 'has nil fork_info if user cannot fork' do
@@ -81,13 +88,13 @@ RSpec.describe IdeController, feature_category: :web_ide do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:project)).to eq project
- expect(assigns(:fork_info)).to eq({ ide_path: controller.helpers.ide_edit_path(fork, branch, '') })
+ expect(assigns(:fork_info)).to eq({ ide_path: controller.helpers.ide_edit_path(fork, '', '') })
end
end
end
end
- context 'user cannot read project' do
+ context 'when user cannot read project' do
let(:user) { other_user }
it 'returns 404' do
@@ -98,7 +105,7 @@ RSpec.describe IdeController, feature_category: :web_ide do
end
end
- context '/-/ide' do
+ context 'with /-/ide' do
let(:route) { '/-/ide' }
it 'returns 404' do
@@ -108,7 +115,7 @@ RSpec.describe IdeController, feature_category: :web_ide do
end
end
- context '/-/ide/project' do
+ context 'with /-/ide/project' do
let(:route) { '/-/ide/project' }
it 'returns 404' do
@@ -118,7 +125,7 @@ RSpec.describe IdeController, feature_category: :web_ide do
end
end
- context '/-/ide/project/:project' do
+ context 'with /-/ide/project/:project' do
let(:route) { "/-/ide/project/#{project.full_path}" }
it 'instantiates project instance var and returns 200' do
@@ -126,16 +133,13 @@ RSpec.describe IdeController, feature_category: :web_ide do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to be_nil
- expect(assigns(:path)).to be_nil
- expect(assigns(:merge_request)).to be_nil
expect(assigns(:fork_info)).to be_nil
end
it_behaves_like 'user access rights check'
- %w(edit blob tree).each do |action|
- context "/-/ide/project/:project/#{action}" do
+ %w[edit blob tree].each do |action|
+ context "with /-/ide/project/:project/#{action}" do
let(:route) { "/-/ide/project/#{project.full_path}/#{action}" }
it 'instantiates project instance var and returns 200' do
@@ -143,89 +147,13 @@ RSpec.describe IdeController, feature_category: :web_ide do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to be_nil
- expect(assigns(:path)).to be_nil
- expect(assigns(:merge_request)).to be_nil
expect(assigns(:fork_info)).to be_nil
end
it_behaves_like 'user access rights check'
-
- context "/-/ide/project/:project/#{action}/:branch" do
- let(:branch) { 'master' }
- let(:route) { "/-/ide/project/#{project.full_path}/#{action}/#{branch}" }
-
- it 'instantiates project and branch instance vars and returns 200' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to eq branch
- expect(assigns(:path)).to be_nil
- expect(assigns(:merge_request)).to be_nil
- expect(assigns(:fork_info)).to be_nil
- end
-
- it_behaves_like 'user access rights check'
-
- context "/-/ide/project/:project/#{action}/:branch/-" do
- let(:branch) { 'branch/slash' }
- let(:route) { "/-/ide/project/#{project.full_path}/#{action}/#{branch}/-" }
-
- it 'instantiates project and branch instance vars and returns 200' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to eq branch
- expect(assigns(:path)).to be_nil
- expect(assigns(:merge_request)).to be_nil
- expect(assigns(:fork_info)).to be_nil
- end
-
- it_behaves_like 'user access rights check'
-
- context "/-/ide/project/:project/#{action}/:branch/-/:path" do
- let(:branch) { 'master' }
- let(:route) { "/-/ide/project/#{project.full_path}/#{action}/#{branch}/-/foo/.bar" }
-
- it 'instantiates project, branch, and path instance vars and returns 200' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to eq branch
- expect(assigns(:path)).to eq 'foo/.bar'
- expect(assigns(:merge_request)).to be_nil
- expect(assigns(:fork_info)).to be_nil
- end
-
- it_behaves_like 'user access rights check'
- end
- end
- end
end
end
- context '/-/ide/project/:project/merge_requests/:merge_request_id' do
- let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
-
- let(:route) { "/-/ide/project/#{project.full_path}/merge_requests/#{merge_request.id}" }
-
- it 'instantiates project and merge_request instance vars and returns 200' do
- subject
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:project)).to eq project
- expect(assigns(:branch)).to be_nil
- expect(assigns(:path)).to be_nil
- expect(assigns(:merge_request)).to eq merge_request.id.to_s
- expect(assigns(:fork_info)).to be_nil
- end
-
- it_behaves_like 'user access rights check'
- end
-
describe 'Snowplow view event', :snowplow do
it 'is tracked' do
subject
@@ -237,33 +165,18 @@ RSpec.describe IdeController, feature_category: :web_ide do
user: user
)
end
-
- context 'when route_hll_to_snowplow_phase2 FF is disabled' do
- before do
- stub_feature_flags(route_hll_to_snowplow_phase2: false)
- end
-
- it 'does not track Snowplow event' do
- subject
-
- expect_no_snowplow_event
- end
- end
end
# This indirectly tests that `minimal: true` was passed to the fullscreen layout
describe 'layout' do
- where(:ff_state, :use_legacy_web_ide, :expect_top_nav) do
- false | false | true
- false | true | true
- true | true | true
- true | false | false
+ where(:ff_state, :expect_top_nav) do
+ false | true
+ true | false
end
with_them do
before do
stub_feature_flags(vscode_web_ide: ff_state)
- allow(user).to receive(:use_legacy_web_ide).and_return(use_legacy_web_ide)
subject
end
@@ -279,15 +192,23 @@ RSpec.describe IdeController, feature_category: :web_ide do
end
end
- describe 'frame-src content security policy' do
+ describe 'content security policy' do
let(:route) { '/-/ide' }
- before do
+ it 'updates the content security policy with the correct frame sources' do
subject
+
+ expect(find_csp_source('frame-src')).to include("http://www.example.com/assets/webpack/", "https://*.vscode-cdn.net/")
+ expect(find_csp_source('worker-src')).to include("http://www.example.com/assets/webpack/")
end
- it 'adds https://*.vscode-cdn.net in frame-src CSP policy' do
- expect(find_csp_frame_src).to include("https://*.vscode-cdn.net/")
+ it 'with relative_url_root, updates the content security policy with the correct frame sources' do
+ stub_config_setting(relative_url_root: '/gitlab')
+
+ subject
+
+ expect(find_csp_source('frame-src')).to include("http://www.example.com/gitlab/assets/webpack/")
+ expect(find_csp_source('worker-src')).to include("http://www.example.com/gitlab/assets/webpack/")
end
end
end
diff --git a/spec/requests/import/github_controller_spec.rb b/spec/requests/import/github_controller_spec.rb
new file mode 100644
index 00000000000..8d57c2895de
--- /dev/null
+++ b/spec/requests/import/github_controller_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Import::GithubController, feature_category: :importers do
+ describe 'GET details' do
+ subject { get details_import_github_path }
+
+ let_it_be(:user) { create(:user) }
+
+ before do
+ stub_application_setting(import_sources: ['github'])
+
+ login_as(user)
+ end
+
+ context 'with feature enabled' do
+ before do
+ stub_feature_flags(import_details_page: true)
+
+ subject
+ end
+
+ it 'responds with a 200 and shows the template' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:details)
+ end
+ end
+
+ context 'with feature disabled' do
+ before do
+ stub_feature_flags(import_details_page: false)
+
+ subject
+ end
+
+ it 'responds with a 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/requests/import/github_groups_controller_spec.rb b/spec/requests/import/github_groups_controller_spec.rb
index 6393dd35a98..dada84758f3 100644
--- a/spec/requests/import/github_groups_controller_spec.rb
+++ b/spec/requests/import/github_groups_controller_spec.rb
@@ -11,6 +11,8 @@ RSpec.describe Import::GithubGroupsController, feature_category: :importers do
let(:params) { {} }
before do
+ stub_application_setting(import_sources: ['github'])
+
login_as(user)
end
diff --git a/spec/requests/import/gitlab_projects_controller_spec.rb b/spec/requests/import/gitlab_projects_controller_spec.rb
index b2c2d306e53..732851c7828 100644
--- a/spec/requests/import/gitlab_projects_controller_spec.rb
+++ b/spec/requests/import/gitlab_projects_controller_spec.rb
@@ -12,6 +12,8 @@ RSpec.describe Import::GitlabProjectsController, feature_category: :importers do
before do
login_as(user)
+
+ stub_application_setting(import_sources: ['gitlab_project'])
end
describe 'POST create' do
@@ -90,4 +92,16 @@ RSpec.describe Import::GitlabProjectsController, feature_category: :importers do
subject { post authorize_import_gitlab_project_path, headers: workhorse_headers }
end
end
+
+ describe 'GET new' do
+ context 'when the user is not allowed to import projects' do
+ let!(:group) { create(:group).tap { |group| group.add_developer(user) } }
+
+ it 'returns 404' do
+ get new_import_gitlab_project_path, params: { namespace_id: group.id }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
end
diff --git a/spec/requests/jira_authorizations_spec.rb b/spec/requests/jira_authorizations_spec.rb
index 8c27b61712c..704db7fba08 100644
--- a/spec/requests/jira_authorizations_spec.rb
+++ b/spec/requests/jira_authorizations_spec.rb
@@ -39,6 +39,16 @@ RSpec.describe 'Jira authorization requests', feature_category: :integrations do
expect(oauth_response_access_token).not_to eql(jira_response_access_token)
end
+ it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do
+ subject do
+ post '/login/oauth/access_token', params: {
+ client_id: client_id,
+ client_secret: client_secret,
+ code: generate_access_grant.token
+ }
+ end
+ end
+
context 'when authorization fails' do
before do
post '/login/oauth/access_token', params: {
diff --git a/spec/requests/jira_connect/oauth_application_ids_controller_spec.rb b/spec/requests/jira_connect/oauth_application_ids_controller_spec.rb
index d111edd06da..2f6113c6dd7 100644
--- a/spec/requests/jira_connect/oauth_application_ids_controller_spec.rb
+++ b/spec/requests/jira_connect/oauth_application_ids_controller_spec.rb
@@ -38,11 +38,7 @@ RSpec.describe JiraConnect::OauthApplicationIdsController, feature_category: :in
end
end
- context 'on GitLab.com' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
+ context 'on SaaS', :saas do
it 'renders not found' do
get '/-/jira_connect/oauth_application_id'
diff --git a/spec/requests/jira_connect/public_keys_controller_spec.rb b/spec/requests/jira_connect/public_keys_controller_spec.rb
index 7f0262eaf65..62a81d43e65 100644
--- a/spec/requests/jira_connect/public_keys_controller_spec.rb
+++ b/spec/requests/jira_connect/public_keys_controller_spec.rb
@@ -5,11 +5,10 @@ require 'spec_helper'
RSpec.describe JiraConnect::PublicKeysController, feature_category: :integrations do
describe 'GET /-/jira_connect/public_keys/:uuid' do
let(:uuid) { non_existing_record_id }
- let(:public_key_storage_enabled_config) { true }
+ let(:public_key_storage_enabled) { true }
before do
- allow(Gitlab.config.jira_connect).to receive(:enable_public_keys_storage)
- .and_return(public_key_storage_enabled_config)
+ stub_application_setting(jira_connect_public_key_storage_enabled: public_key_storage_enabled)
end
it 'renders 404' do
@@ -30,26 +29,14 @@ RSpec.describe JiraConnect::PublicKeysController, feature_category: :integration
expect(response.body).to eq(public_key.key)
end
- context 'when public key storage config disabled' do
- let(:public_key_storage_enabled_config) { false }
+ context 'when public key storage setting disabled' do
+ let(:public_key_storage_enabled) { false }
it 'renders 404' do
get jira_connect_public_key_path(id: uuid)
expect(response).to have_gitlab_http_status(:not_found)
end
-
- context 'when public key storage setting is enabled' do
- before do
- stub_application_setting(jira_connect_public_key_storage_enabled: true)
- end
-
- it 'renders 404' do
- get jira_connect_public_key_path(id: uuid)
-
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
end
end
end
diff --git a/spec/requests/jira_connect/users_controller_spec.rb b/spec/requests/jira_connect/users_controller_spec.rb
deleted file mode 100644
index c02bd324708..00000000000
--- a/spec/requests/jira_connect/users_controller_spec.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe JiraConnect::UsersController, feature_category: :integrations do
- describe 'GET /-/jira_connect/users' do
- let_it_be(:user) { create(:user) }
-
- before do
- sign_in(user)
- end
-
- context 'with a valid host' do
- let(:return_to) { 'https://testcompany.atlassian.net/plugins/servlet/ac/gitlab-jira-connect-staging.gitlab.com/gitlab-configuration' }
-
- it 'includes a return url' do
- get '/-/jira_connect/users', params: { return_to: return_to }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to include('Return to GitLab')
- end
- end
-
- context 'with an invalid host' do
- let(:return_to) { 'https://evil.com' }
-
- it 'does not include a return url' do
- get '/-/jira_connect/users', params: { return_to: return_to }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).not_to include('Return to GitLab')
- end
- end
-
- context 'with a script injected' do
- let(:return_to) { 'javascript://test.atlassian.net/%250dalert(document.domain)' }
-
- it 'does not include a return url' do
- get '/-/jira_connect/users', params: { return_to: return_to }
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).not_to include('Return to GitLab')
- end
- end
- end
-end
diff --git a/spec/requests/jwks_controller_spec.rb b/spec/requests/jwks_controller_spec.rb
index ac9765c35d8..f756c1758e4 100644
--- a/spec/requests/jwks_controller_spec.rb
+++ b/spec/requests/jwks_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JwksController, feature_category: :authentication_and_authorization do
+RSpec.describe JwksController, feature_category: :system_access do
describe 'Endpoints from the parent Doorkeeper::OpenidConnect::DiscoveryController' do
it 'respond successfully' do
[
@@ -35,6 +35,15 @@ RSpec.describe JwksController, feature_category: :authentication_and_authorizati
expect(ids).to contain_exactly(ci_jwk['kid'], oidc_jwk['kid'])
end
+ it 'includes the OIDC signing key ID' do
+ get jwks_url
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ ids = json_response['keys'].map { |jwk| jwk['kid'] }
+ expect(ids).to include(Doorkeeper::OpenidConnect.signing_key_normalized.symbolize_keys[:kid])
+ end
+
it 'does not leak private key data' do
get jwks_url
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index 00222cb1977..69127a7526e 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe JwtController, feature_category: :authentication_and_authorization do
+RSpec.describe JwtController, feature_category: :system_access do
include_context 'parsed logs'
let(:service) { double(execute: {} ) }
@@ -53,6 +53,14 @@ RSpec.describe JwtController, feature_category: :authentication_and_authorizatio
end
end
+ context 'POST /jwt/auth' do
+ it 'returns 404' do
+ post '/jwt/auth'
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
context 'authenticating against container registry' do
context 'existing service' do
subject! { get '/jwt/auth', params: parameters }
diff --git a/spec/requests/oauth/applications_controller_spec.rb b/spec/requests/oauth/applications_controller_spec.rb
index 94ee08f6272..8c2856b87d1 100644
--- a/spec/requests/oauth/applications_controller_spec.rb
+++ b/spec/requests/oauth/applications_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Oauth::ApplicationsController, feature_category: :authentication_and_authorization do
+RSpec.describe Oauth::ApplicationsController, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:application) { create(:oauth_application, owner: user) }
let_it_be(:show_path) { oauth_application_path(application) }
diff --git a/spec/requests/oauth/authorizations_controller_spec.rb b/spec/requests/oauth/authorizations_controller_spec.rb
index 52188717210..257f238d9ef 100644
--- a/spec/requests/oauth/authorizations_controller_spec.rb
+++ b/spec/requests/oauth/authorizations_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Oauth::AuthorizationsController, feature_category: :authentication_and_authorization do
+RSpec.describe Oauth::AuthorizationsController, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:application) { create(:oauth_application, redirect_uri: 'custom://test') }
let_it_be(:oauth_authorization_path) do
diff --git a/spec/requests/oauth/tokens_controller_spec.rb b/spec/requests/oauth/tokens_controller_spec.rb
index cdfad8cb59c..58203a81bac 100644
--- a/spec/requests/oauth/tokens_controller_spec.rb
+++ b/spec/requests/oauth/tokens_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Oauth::TokensController, feature_category: :authentication_and_authorization do
+RSpec.describe Oauth::TokensController, feature_category: :system_access do
let(:cors_request_headers) { { 'Origin' => 'http://notgitlab.com' } }
let(:other_headers) { {} }
let(:headers) { cors_request_headers.merge(other_headers) }
diff --git a/spec/requests/oauth_tokens_spec.rb b/spec/requests/oauth_tokens_spec.rb
index 053bd317fcc..67c676fdb40 100644
--- a/spec/requests/oauth_tokens_spec.rb
+++ b/spec/requests/oauth_tokens_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'OAuth Tokens requests', feature_category: :authentication_and_authorization do
+RSpec.describe 'OAuth Tokens requests', feature_category: :system_access do
let(:user) { create :user }
let(:application) { create :oauth_application, scopes: 'api' }
let(:grant_type) { 'authorization_code' }
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index 9035e723abe..82f972e7f94 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'OpenID Connect requests', feature_category: :authentication_and_authorization do
+RSpec.describe 'OpenID Connect requests', feature_category: :system_access do
let(:user) do
create(
:user,
@@ -276,7 +276,7 @@ RSpec.describe 'OpenID Connect requests', feature_category: :authentication_and_
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['issuer']).to eq('http://localhost')
expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys')
- expect(json_response['scopes_supported']).to match_array %w[admin_mode api read_user read_api read_repository write_repository sudo openid profile email]
+ expect(json_response['scopes_supported']).to match_array %w[admin_mode api read_user read_api read_repository write_repository sudo openid profile email read_observability write_observability]
end
context 'with a cross-origin request' do
@@ -286,7 +286,7 @@ RSpec.describe 'OpenID Connect requests', feature_category: :authentication_and_
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['issuer']).to eq('http://localhost')
expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys')
- expect(json_response['scopes_supported']).to match_array %w[admin_mode api read_user read_api read_repository write_repository sudo openid profile email]
+ expect(json_response['scopes_supported']).to match_array %w[admin_mode api read_user read_api read_repository write_repository sudo openid profile email read_observability write_observability]
end
it_behaves_like 'cross-origin GET request'
diff --git a/spec/requests/profiles/saved_replies_controller_spec.rb b/spec/requests/profiles/comment_templates_controller_spec.rb
index 27a961a201f..cdbfbb0a346 100644
--- a/spec/requests/profiles/saved_replies_controller_spec.rb
+++ b/spec/requests/profiles/comment_templates_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Profiles::SavedRepliesController, feature_category: :user_profile do
+RSpec.describe Profiles::CommentTemplatesController, feature_category: :user_profile do
let_it_be(:user) { create(:user) }
before do
@@ -14,7 +14,7 @@ RSpec.describe Profiles::SavedRepliesController, feature_category: :user_profile
before do
stub_feature_flags(saved_replies: false)
- get '/-/profile/saved_replies'
+ get '/-/profile/comment_templates'
end
it { expect(response).to have_gitlab_http_status(:not_found) }
@@ -22,7 +22,7 @@ RSpec.describe Profiles::SavedRepliesController, feature_category: :user_profile
describe 'feature flag enabled' do
before do
- get '/-/profile/saved_replies'
+ get '/-/profile/comment_templates'
end
it { expect(response).to have_gitlab_http_status(:ok) }
diff --git a/spec/requests/projects/airflow/dags_controller_spec.rb b/spec/requests/projects/airflow/dags_controller_spec.rb
deleted file mode 100644
index 2dcedf5f128..00000000000
--- a/spec/requests/projects/airflow/dags_controller_spec.rb
+++ /dev/null
@@ -1,105 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Projects::Airflow::DagsController, feature_category: :dataops do
- let_it_be(:non_member) { create(:user) }
- let_it_be(:user) { create(:user) }
- let_it_be(:group) { create(:group).tap { |p| p.add_developer(user) } }
- let_it_be(:project) { create(:project, group: group).tap { |p| p.add_developer(user) } }
-
- let(:current_user) { user }
- let(:feature_flag) { true }
-
- let_it_be(:dags) do
- create_list(:airflow_dags, 5, project: project)
- end
-
- let(:params) { { namespace_id: project.namespace.to_param, project_id: project } }
- let(:extra_params) { {} }
-
- before do
- sign_in(current_user) if current_user
- stub_feature_flags(airflow_dags: false)
- stub_feature_flags(airflow_dags: project) if feature_flag
- list_dags
- end
-
- shared_examples 'returns a 404 if feature flag disabled' do
- context 'when :airflow_dags disabled' do
- let(:feature_flag) { false }
-
- it 'is 404' do
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- describe 'GET index' do
- it 'renders the template' do
- expect(response).to render_template('projects/airflow/dags/index')
- end
-
- describe 'pagination' do
- before do
- stub_const("Projects::Airflow::DagsController::MAX_DAGS_PER_PAGE", 2)
- dags
-
- list_dags
- end
-
- context 'when out of bounds' do
- let(:params) { extra_params.merge(page: 10000) }
-
- it 'redirects to last page' do
- last_page = (dags.size + 1) / 2
- expect(response).to redirect_to(project_airflow_dags_path(project, page: last_page))
- end
- end
-
- context 'when bad page' do
- let(:params) { extra_params.merge(page: 's') }
-
- it 'uses first page' do
- expect(assigns(:pagination)).to include(
- page: 1,
- is_last_page: false,
- per_page: 2,
- total_items: dags.size)
- end
- end
- end
-
- it 'does not perform N+1 sql queries' do
- control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { list_dags }
-
- create_list(:airflow_dags, 1, project: project)
-
- expect { list_dags }.not_to exceed_all_query_limit(control_count)
- end
-
- context 'when user is not logged in' do
- let(:current_user) { nil }
-
- it 'redirects to login' do
- expect(response).to redirect_to(new_user_session_path)
- end
- end
-
- context 'when user is not a member' do
- let(:current_user) { non_member }
-
- it 'returns a 404' do
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- it_behaves_like 'returns a 404 if feature flag disabled'
- end
-
- private
-
- def list_dags
- get project_airflow_dags_path(project), params: params
- end
-end
diff --git a/spec/requests/projects/aws/configuration_controller_spec.rb b/spec/requests/projects/aws/configuration_controller_spec.rb
new file mode 100644
index 00000000000..af9460eb76c
--- /dev/null
+++ b/spec/requests/projects/aws/configuration_controller_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::Aws::ConfigurationController, feature_category: :five_minute_production_app do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:url) { project_aws_configuration_path(project) }
+
+ let_it_be(:user_guest) { create(:user) }
+ let_it_be(:user_developer) { create(:user) }
+ let_it_be(:user_maintainer) { create(:user) }
+
+ let_it_be(:unauthorized_members) { [user_guest, user_developer] }
+ let_it_be(:authorized_members) { [user_maintainer] }
+
+ before do
+ project.add_guest(user_guest)
+ project.add_developer(user_developer)
+ project.add_maintainer(user_maintainer)
+ end
+
+ context 'when accessed by unauthorized members' do
+ it 'returns not found on GET request' do
+ unauthorized_members.each do |unauthorized_member|
+ sign_in(unauthorized_member)
+
+ get url
+ expect_snowplow_event(
+ category: 'Projects::Aws::ConfigurationController',
+ action: 'error_invalid_user',
+ label: nil,
+ project: project,
+ user: unauthorized_member
+ )
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when accessed by authorized members' do
+ it 'returns successful' do
+ authorized_members.each do |authorized_member|
+ sign_in(authorized_member)
+
+ get url
+
+ expect(response).to be_successful
+ expect(response).to render_template('projects/aws/configuration/index')
+ end
+ end
+
+ include_examples 'requires feature flag `cloudseed_aws` enabled' do
+ subject { get url }
+
+ let_it_be(:user) { user_maintainer }
+ end
+ end
+end
diff --git a/spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb b/spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb
index b0c7427fa81..11f962e0e96 100644
--- a/spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb
+++ b/spec/requests/projects/ci/promeheus_metrics/histograms_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Projects::Ci::PrometheusMetrics::HistogramsController', feature_category: :pipeline_authoring do
+RSpec.describe 'Projects::Ci::PrometheusMetrics::HistogramsController', feature_category: :pipeline_composition do
let_it_be(:project) { create(:project, :public) }
describe 'POST /*namespace_id/:project_id/-/ci/prometheus_metrics/histograms' do
diff --git a/spec/requests/projects/cluster_agents_controller_spec.rb b/spec/requests/projects/cluster_agents_controller_spec.rb
index d7c791fa0c1..643160ad9f3 100644
--- a/spec/requests/projects/cluster_agents_controller_spec.rb
+++ b/spec/requests/projects/cluster_agents_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::ClusterAgentsController, feature_category: :kubernetes_management do
+RSpec.describe Projects::ClusterAgentsController, feature_category: :deployment_management do
let_it_be(:cluster_agent) { create(:cluster_agent) }
let(:project) { cluster_agent.project }
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
index 3f9dd74c145..0adf0b525a9 100644
--- a/spec/requests/projects/cycle_analytics_events_spec.rb
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'value stream analytics events', feature_category: :planning_analytics do
+RSpec.describe 'value stream analytics events', feature_category: :team_planning do
include CycleAnalyticsHelpers
let(:user) { create(:user) }
diff --git a/spec/requests/projects/environments_controller_spec.rb b/spec/requests/projects/environments_controller_spec.rb
index 41ae2d434fa..5dd83fedf8d 100644
--- a/spec/requests/projects/environments_controller_spec.rb
+++ b/spec/requests/projects/environments_controller_spec.rb
@@ -18,9 +18,7 @@ RSpec.describe Projects::EnvironmentsController, feature_category: :continuous_d
end
def environment_params(opts = {})
- opts.reverse_merge(namespace_id: project.namespace,
- project_id: project,
- id: environment.id)
+ opts.reverse_merge(namespace_id: project.namespace, project_id: project, id: environment.id)
end
def create_deployment_with_associations(commit_depth:)
diff --git a/spec/requests/projects/google_cloud/configuration_controller_spec.rb b/spec/requests/projects/google_cloud/configuration_controller_spec.rb
index 1aa44d1a49a..b807ff7930e 100644
--- a/spec/requests/projects/google_cloud/configuration_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/configuration_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::GoogleCloud::ConfigurationController, feature_category: :kubernetes_management do
+RSpec.describe Projects::GoogleCloud::ConfigurationController, feature_category: :deployment_management do
let_it_be(:project) { create(:project, :public) }
let_it_be(:url) { project_google_cloud_configuration_path(project) }
diff --git a/spec/requests/projects/google_cloud/databases_controller_spec.rb b/spec/requests/projects/google_cloud/databases_controller_spec.rb
index 98e83610600..fa978a3921f 100644
--- a/spec/requests/projects/google_cloud/databases_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/databases_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::GoogleCloud::DatabasesController, :snowplow, feature_category: :kubernetes_management do
+RSpec.describe Projects::GoogleCloud::DatabasesController, :snowplow, feature_category: :deployment_management do
shared_examples 'shared examples for database controller endpoints' do
include_examples 'requires `admin_project_google_cloud` role'
diff --git a/spec/requests/projects/google_cloud/deployments_controller_spec.rb b/spec/requests/projects/google_cloud/deployments_controller_spec.rb
index d564a31f835..e9eac1e7ecd 100644
--- a/spec/requests/projects/google_cloud/deployments_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/deployments_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::GoogleCloud::DeploymentsController, feature_category: :kubernetes_management do
+RSpec.describe Projects::GoogleCloud::DeploymentsController, feature_category: :deployment_management do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:repository) { project.repository }
@@ -108,66 +108,104 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController, feature_category: :
end
end
- it 'redirects to google cloud deployments on enable service error' do
- get url
-
- expect(response).to redirect_to(project_google_cloud_deployments_path(project))
- # since GPC_PROJECT_ID is not set, enable cloud run service should return an error
- expect_snowplow_event(
- category: 'Projects::GoogleCloud::DeploymentsController',
- action: 'error_enable_services',
- label: nil,
- project: project,
- user: user_maintainer
- )
- end
+ context 'when enable service fails' do
+ before do
+ allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service|
+ allow(service)
+ .to receive(:execute)
+ .and_return(
+ status: :error,
+ message: 'No GCP projects found. Configure a service account or GCP_PROJECT_ID ci variable'
+ )
+ end
+ end
- it 'redirects to google cloud deployments with error' do
- mock_gcp_error = Google::Apis::ClientError.new('some_error')
+ it 'redirects to google cloud deployments and tracks event on enable service error' do
+ get url
- allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service|
- allow(service).to receive(:execute).and_raise(mock_gcp_error)
+ expect(response).to redirect_to(project_google_cloud_deployments_path(project))
+ # since GPC_PROJECT_ID is not set, enable cloud run service should return an error
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud::DeploymentsController',
+ action: 'error_enable_services',
+ label: nil,
+ project: project,
+ user: user_maintainer
+ )
end
- get url
+ it 'shows a flash alert' do
+ get url
- expect(response).to redirect_to(project_google_cloud_deployments_path(project))
- expect_snowplow_event(
- category: 'Projects::GoogleCloud::DeploymentsController',
- action: 'error_google_api',
- label: nil,
- project: project,
- user: user_maintainer
- )
+ expect(flash[:alert])
+ .to eq('No GCP projects found. Configure a service account or GCP_PROJECT_ID ci variable')
+ end
end
- context 'GCP_PROJECT_IDs are defined' do
- it 'redirects to google_cloud deployments on generate pipeline error' do
- allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |enable_cloud_run_service|
- allow(enable_cloud_run_service).to receive(:execute).and_return({ status: :success })
- end
+ context 'when enable service raises an error' do
+ before do
+ mock_gcp_error = Google::Apis::ClientError.new('some_error')
- allow_next_instance_of(GoogleCloud::GeneratePipelineService) do |generate_pipeline_service|
- allow(generate_pipeline_service).to receive(:execute).and_return({ status: :error })
+ allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service|
+ allow(service).to receive(:execute).and_raise(mock_gcp_error)
end
+ end
+ it 'redirects to google cloud deployments with error' do
get url
expect(response).to redirect_to(project_google_cloud_deployments_path(project))
expect_snowplow_event(
category: 'Projects::GoogleCloud::DeploymentsController',
- action: 'error_generate_cloudrun_pipeline',
+ action: 'error_google_api',
label: nil,
project: project,
user: user_maintainer
)
end
- it 'redirects to create merge request form' do
- allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service|
- allow(service).to receive(:execute).and_return({ status: :success })
+ it 'shows a flash warning' do
+ get url
+
+ expect(flash[:warning]).to eq(format(_('Google Cloud Error - %{error}'), error: 'some_error'))
+ end
+ end
+
+ context 'GCP_PROJECT_IDs are defined' do
+ before do
+ allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |enable_cloud_run_service|
+ allow(enable_cloud_run_service).to receive(:execute).and_return({ status: :success })
+ end
+ end
+
+ context 'when generate pipeline service fails' do
+ before do
+ allow_next_instance_of(GoogleCloud::GeneratePipelineService) do |generate_pipeline_service|
+ allow(generate_pipeline_service).to receive(:execute).and_return({ status: :error })
+ end
+ end
+
+ it 'redirects to google_cloud deployments and tracks event on generate pipeline error' do
+ get url
+
+ expect(response).to redirect_to(project_google_cloud_deployments_path(project))
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud::DeploymentsController',
+ action: 'error_generate_cloudrun_pipeline',
+ label: nil,
+ project: project,
+ user: user_maintainer
+ )
+ end
+
+ it 'shows a flash alert' do
+ get url
+
+ expect(flash[:alert]).to eq('Failed to generate pipeline')
end
+ end
+ it 'redirects to create merge request form' do
allow_next_instance_of(GoogleCloud::GeneratePipelineService) do |service|
allow(service).to receive(:execute).and_return({ status: :success })
end
diff --git a/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb b/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb
index de4b96a2e01..da000ec00c0 100644
--- a/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::GoogleCloud::GcpRegionsController, feature_category: :kubernetes_management do
+RSpec.describe Projects::GoogleCloud::GcpRegionsController, feature_category: :deployment_management do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:repository) { project.repository }
diff --git a/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb b/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb
index 5965953cf6f..427eff8cd76 100644
--- a/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::GoogleCloud::RevokeOauthController, feature_category: :kubernetes_management do
+RSpec.describe Projects::GoogleCloud::RevokeOauthController, feature_category: :deployment_management do
include SessionHelpers
describe 'POST #create', :snowplow, :clean_gitlab_redis_sessions, :aggregate_failures do
diff --git a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb
index 9b048f814ef..29d4154329f 100644
--- a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::GoogleCloud::ServiceAccountsController, feature_category: :kubernetes_management do
+RSpec.describe Projects::GoogleCloud::ServiceAccountsController, feature_category: :deployment_management do
let_it_be(:project) { create(:project, :public) }
describe 'GET index', :snowplow do
diff --git a/spec/requests/projects/incident_management/timeline_events_spec.rb b/spec/requests/projects/incident_management/timeline_events_spec.rb
index 22a1f654ee2..b827ec07ae1 100644
--- a/spec/requests/projects/incident_management/timeline_events_spec.rb
+++ b/spec/requests/projects/incident_management/timeline_events_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe 'Timeline Events', feature_category: :incident_management do
it 'renders JSON in a correct format' do
post preview_markdown_project_incident_management_timeline_events_path(project, format: :json),
- params: { text: timeline_text }
+ params: { text: timeline_text }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq({
@@ -51,7 +51,7 @@ RSpec.describe 'Timeline Events', feature_category: :incident_management do
context 'when not authorized' do
it 'returns 302' do
post preview_markdown_project_incident_management_timeline_events_path(project, format: :json),
- params: { text: timeline_text }
+ params: { text: timeline_text }
expect(response).to have_gitlab_http_status(:found)
end
diff --git a/spec/requests/projects/issue_links_controller_spec.rb b/spec/requests/projects/issue_links_controller_spec.rb
index 0535156b4b8..c242f762cde 100644
--- a/spec/requests/projects/issue_links_controller_spec.rb
+++ b/spec/requests/projects/issue_links_controller_spec.rb
@@ -28,28 +28,12 @@ RSpec.describe Projects::IssueLinksController, feature_category: :team_planning
context 'when linked issue is a task' do
let(:issue_b) { create :issue, :task, project: project }
- context 'when the use_iid_in_work_items_path feature flag is disabled' do
- before do
- stub_feature_flags(use_iid_in_work_items_path: false)
- end
-
- it 'returns a work item path for the linked task' do
- get namespace_project_issue_links_path(issue_links_params)
-
- expect(json_response.count).to eq(1)
- expect(json_response.first).to include(
- 'path' => project_work_items_path(issue_b.project, issue_b.id),
- 'type' => 'TASK'
- )
- end
- end
-
it 'returns a work item path for the linked task using the iid in the path' do
get namespace_project_issue_links_path(issue_links_params)
expect(json_response.count).to eq(1)
expect(json_response.first).to include(
- 'path' => project_work_items_path(issue_b.project, issue_b.iid, iid_path: true),
+ 'path' => project_work_items_path(issue_b.project, issue_b.iid),
'type' => 'TASK'
)
end
@@ -74,8 +58,7 @@ RSpec.describe Projects::IssueLinksController, feature_category: :team_planning
list_service_response = IssueLinks::ListService.new(issue, user).execute
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq('message' => nil,
- 'issuables' => list_service_response.as_json)
+ expect(json_response).to eq('message' => nil, 'issuables' => list_service_response.as_json)
end
end
@@ -178,9 +161,6 @@ RSpec.describe Projects::IssueLinksController, feature_category: :team_planning
end
def issue_links_params(opts = {})
- opts.reverse_merge(namespace_id: issue.project.namespace,
- project_id: issue.project,
- issue_id: issue,
- format: :json)
+ opts.reverse_merge(namespace_id: issue.project.namespace, project_id: issue.project, issue_id: issue, format: :json)
end
end
diff --git a/spec/requests/projects/issues_controller_spec.rb b/spec/requests/projects/issues_controller_spec.rb
index 67a73834f2d..583fd5f586e 100644
--- a/spec/requests/projects/issues_controller_spec.rb
+++ b/spec/requests/projects/issues_controller_spec.rb
@@ -25,33 +25,28 @@ RSpec.describe Projects::IssuesController, feature_category: :team_planning do
end
describe 'GET #show' do
- include_context 'group project issue'
+ before do
+ login_as(user)
+ end
it_behaves_like "observability csp policy", described_class do
+ include_context 'group project issue'
let(:tested_path) do
project_issue_path(project, issue)
end
end
- end
- describe 'GET #index.json' do
- let_it_be(:public_project) { create(:project, :public) }
+ describe 'incident tabs' do
+ let_it_be(:incident) { create(:incident, project: project) }
- it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit do
- let_it_be(:current_user) { create(:user) }
-
- before do
- sign_in current_user
- end
-
- def request
- get project_issues_path(public_project, format: :json), params: { scope: 'all', search: 'test' }
+ it 'redirects to the issues route for non-incidents' do
+ get incident_issue_project_issue_path(project, issue, 'timeline')
+ expect(response).to redirect_to project_issue_path(project, issue)
end
- end
- it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit_unauthenticated do
- def request
- get project_issues_path(public_project, format: :json), params: { scope: 'all', search: 'test' }
+ it 'responds with selected tab for incidents' do
+ get incident_issue_project_issue_path(project, incident, 'timeline')
+ expect(response.body).to match(/&quot;currentTab&quot;:&quot;timeline&quot;/)
end
end
end
@@ -119,8 +114,9 @@ RSpec.describe Projects::IssuesController, feature_category: :team_planning do
context 'when private project' do
let_it_be(:private_project) { create(:project, :private) }
- it_behaves_like 'authenticates sessionless user for the request spec', 'index atom', public_resource: false,
-ignore_metrics: true do
+ it_behaves_like 'authenticates sessionless user for the request spec', 'index atom',
+ public_resource: false,
+ ignore_metrics: true do
let(:url) { project_issues_url(private_project, format: :atom) }
before do
@@ -128,8 +124,9 @@ ignore_metrics: true do
end
end
- it_behaves_like 'authenticates sessionless user for the request spec', 'calendar ics', public_resource: false,
-ignore_metrics: true do
+ it_behaves_like 'authenticates sessionless user for the request spec', 'calendar ics',
+ public_resource: false,
+ ignore_metrics: true do
let(:url) { project_issues_url(private_project, format: :ics) }
before do
diff --git a/spec/requests/projects/merge_requests_controller_spec.rb b/spec/requests/projects/merge_requests_controller_spec.rb
index f441438a95a..955e6822211 100644
--- a/spec/requests/projects/merge_requests_controller_spec.rb
+++ b/spec/requests/projects/merge_requests_controller_spec.rb
@@ -120,8 +120,9 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :source_code
context 'when private project' do
let_it_be(:private_project) { create(:project, :private) }
- it_behaves_like 'authenticates sessionless user for the request spec', 'index atom', public_resource: false,
- ignore_metrics: true do
+ it_behaves_like 'authenticates sessionless user for the request spec', 'index atom',
+ public_resource: false,
+ ignore_metrics: true do
let(:url) { project_merge_requests_url(private_project, format: :atom) }
before do
diff --git a/spec/requests/projects/merge_requests_discussions_spec.rb b/spec/requests/projects/merge_requests_discussions_spec.rb
index d82fa284a42..caf62c251b6 100644
--- a/spec/requests/projects/merge_requests_discussions_spec.rb
+++ b/spec/requests/projects/merge_requests_discussions_spec.rb
@@ -27,6 +27,21 @@ RSpec.describe 'merge requests discussions', feature_category: :source_code_mana
end
# rubocop:enable RSpec/InstanceVariable
+ shared_examples 'N+1 queries' do
+ it 'avoids N+1 DB queries', :request_store do
+ send_request # warm up
+
+ create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project)
+ control = ActiveRecord::QueryRecorder.new { send_request }
+
+ create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project)
+
+ expect do
+ send_request
+ end.not_to exceed_query_limit(control).with_threshold(notes_metadata_threshold)
+ end
+ end
+
it 'returns 200' do
send_request
@@ -34,17 +49,20 @@ RSpec.describe 'merge requests discussions', feature_category: :source_code_mana
end
# https://docs.gitlab.com/ee/development/query_recorder.html#use-request-specs-instead-of-controller-specs
- it 'avoids N+1 DB queries', :request_store do
- send_request # warm up
+ context 'with notes_metadata_threshold' do
+ let(:notes_metadata_threshold) { 1 }
- create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project)
- control = ActiveRecord::QueryRecorder.new { send_request }
+ it_behaves_like 'N+1 queries'
- create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project)
+ context 'when external_note_author_service_desk feature flag is disabled' do
+ let(:notes_metadata_threshold) { 0 }
- expect do
- send_request
- end.not_to exceed_query_limit(control)
+ before do
+ stub_feature_flags(external_note_author_service_desk: false)
+ end
+
+ it_behaves_like 'N+1 queries'
+ end
end
it 'limits Gitaly queries', :request_store do
@@ -59,7 +77,7 @@ RSpec.describe 'merge requests discussions', feature_category: :source_code_mana
.to change { Gitlab::GitalyClient.get_request_count }.by_at_most(4)
end
- context 'caching', :use_clean_rails_memory_store_caching do
+ context 'caching' do
let(:reference) { create(:issue, project: project) }
let(:author) { create(:user) }
let!(:first_note) { create(:diff_note_on_merge_request, author: author, noteable: merge_request, project: project, note: "reference: #{reference.to_reference}") }
@@ -81,193 +99,180 @@ RSpec.describe 'merge requests discussions', feature_category: :source_code_mana
shared_examples 'cache hit' do
it 'gets cached on subsequent requests' do
- expect_next_instance_of(DiscussionSerializer) do |serializer|
- expect(serializer).not_to receive(:represent)
- end
+ expect(DiscussionSerializer).not_to receive(:new)
send_request
end
end
- context 'when mr_discussions_http_cache and disabled_mr_discussions_redis_cache are enabled' do
- before do
- send_request
- end
+ before do
+ send_request
+ end
- it_behaves_like 'cache hit'
+ it_behaves_like 'cache hit'
- context 'when a note in a discussion got updated' do
- before do
- first_note.update!(updated_at: 1.minute.from_now)
- end
-
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ context 'when a note in a discussion got updated' do
+ before do
+ first_note.update!(updated_at: 1.minute.from_now)
end
- context 'when a note in a discussion got its reference state updated' do
- before do
- reference.close!
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
+ end
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ context 'when a note in a discussion got its reference state updated' do
+ before do
+ reference.close!
end
- context 'when a note in a discussion got resolved' do
- before do
- travel_to(1.minute.from_now) do
- first_note.resolve!(user)
- end
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
+ end
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
+ context 'when a note in a discussion got resolved' do
+ before do
+ travel_to(1.minute.from_now) do
+ first_note.resolve!(user)
end
end
- context 'when a note is added to a discussion' do
- let!(:third_note) { create(:diff_note_on_merge_request, in_reply_to: first_note, noteable: merge_request, project: project) }
-
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note, third_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when a note is removed from a discussion' do
- before do
- second_note.destroy!
- end
+ context 'when a note is added to a discussion' do
+ let!(:third_note) { create(:diff_note_on_merge_request, in_reply_to: first_note, noteable: merge_request, project: project) }
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note, third_note] }
end
+ end
- context 'when an emoji is awarded to a note in discussion' do
- before do
- travel_to(1.minute.from_now) do
- create(:award_emoji, awardable: first_note)
- end
- end
+ context 'when a note is removed from a discussion' do
+ before do
+ second_note.destroy!
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note] }
end
+ end
- context 'when an award emoji is removed from a note in discussion' do
- before do
- travel_to(1.minute.from_now) do
- award_emoji.destroy!
- end
+ context 'when an emoji is awarded to a note in discussion' do
+ before do
+ travel_to(1.minute.from_now) do
+ create(:award_emoji, awardable: first_note)
end
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when the diff note position changes' do
- before do
- # This replicates a position change wherein timestamps aren't updated
- # which is why `Gitlab::Timeless.timeless` is utilized. This is the
- # same approach being used in Discussions::UpdateDiffPositionService
- # which is responsible for updating the positions of diff discussions
- # when MR updates.
- first_note.position = Gitlab::Diff::Position.new(
- old_path: first_note.position.old_path,
- new_path: first_note.position.new_path,
- old_line: first_note.position.old_line,
- new_line: first_note.position.new_line + 1,
- diff_refs: first_note.position.diff_refs
- )
-
- Gitlab::Timeless.timeless(first_note, &:save)
+ context 'when an award emoji is removed from a note in discussion' do
+ before do
+ travel_to(1.minute.from_now) do
+ award_emoji.destroy!
end
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when the HEAD diff note position changes' do
- before do
- # This replicates a DiffNotePosition change. This is the same approach
- # being used in Discussions::CaptureDiffNotePositionService which is
- # responsible for updating/creating DiffNotePosition of a diff discussions
- # in relation to HEAD diff.
- new_position = Gitlab::Diff::Position.new(
- old_path: first_note.position.old_path,
- new_path: first_note.position.new_path,
- old_line: first_note.position.old_line,
- new_line: first_note.position.new_line + 1,
- diff_refs: first_note.position.diff_refs
- )
-
- DiffNotePosition.create_or_update_for(
- first_note,
- diff_type: :head,
- position: new_position,
- line_code: 'bd4b7bfff3a247ccf6e3371c41ec018a55230bcc_534_521'
- )
- end
+ context 'when the diff note position changes' do
+ before do
+ # This replicates a position change wherein timestamps aren't updated
+ # which is why `save(touch: false)` is utilized. This is the same
+ # approach being used in Discussions::UpdateDiffPositionService which
+ # is responsible for updating the positions of diff discussions when
+ # MR updates.
+ first_note.position = Gitlab::Diff::Position.new(
+ old_path: first_note.position.old_path,
+ new_path: first_note.position.new_path,
+ old_line: first_note.position.old_line,
+ new_line: first_note.position.new_line + 1,
+ diff_refs: first_note.position.diff_refs
+ )
+
+ first_note.save!(touch: false)
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when author detail changes' do
- before do
- author.update!(name: "#{author.name} (Updated)")
- end
+ context 'when the HEAD diff note position changes' do
+ before do
+ # This replicates a DiffNotePosition change. This is the same approach
+ # being used in Discussions::CaptureDiffNotePositionService which is
+ # responsible for updating/creating DiffNotePosition of a diff discussions
+ # in relation to HEAD diff.
+ new_position = Gitlab::Diff::Position.new(
+ old_path: first_note.position.old_path,
+ new_path: first_note.position.new_path,
+ old_line: first_note.position.old_line,
+ new_line: first_note.position.new_line + 1,
+ diff_refs: first_note.position.diff_refs
+ )
+
+ DiffNotePosition.create_or_update_for(
+ first_note,
+ diff_type: :head,
+ position: new_position,
+ line_code: 'bd4b7bfff3a247ccf6e3371c41ec018a55230bcc_534_521'
+ )
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when author status changes' do
- before do
- Users::SetStatusService.new(author, message: "updated status").execute
- end
+ context 'when author detail changes' do
+ before do
+ author.update!(name: "#{author.name} (Updated)")
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when author role changes' do
- before do
- Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(author_membership)
- end
+ context 'when author status changes' do
+ before do
+ Users::SetStatusService.new(author, message: "updated status").execute
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
+ end
- context 'when current_user role changes' do
- before do
- Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(project.member(user))
- end
+ context 'when author role changes' do
+ before do
+ Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(author_membership)
+ end
- it_behaves_like 'cache miss' do
- let(:changed_notes) { [first_note, second_note] }
- end
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
end
end
- context 'when disabled_mr_discussions_redis_cache is disabled' do
+ context 'when current_user role changes' do
before do
- stub_feature_flags(disabled_mr_discussions_redis_cache: false)
- send_request
+ Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(project.member(user))
end
- it_behaves_like 'cache hit'
+ it_behaves_like 'cache miss' do
+ let(:changed_notes) { [first_note, second_note] }
+ end
end
end
end
diff --git a/spec/requests/projects/merge_requests_spec.rb b/spec/requests/projects/merge_requests_spec.rb
index 9600d1a3656..e57808e6728 100644
--- a/spec/requests/projects/merge_requests_spec.rb
+++ b/spec/requests/projects/merge_requests_spec.rb
@@ -6,10 +6,13 @@ RSpec.describe 'merge requests actions', feature_category: :source_code_manageme
let_it_be(:project) { create(:project, :repository) }
let(:merge_request) do
- create(:merge_request_with_diffs, target_project: project,
- source_project: project,
- assignees: [user],
- reviewers: [user2])
+ create(
+ :merge_request_with_diffs,
+ target_project: project,
+ source_project: project,
+ assignees: [user],
+ reviewers: [user2]
+ )
end
let(:user) { project.first_owner }
diff --git a/spec/requests/projects/metrics/dashboards/builder_spec.rb b/spec/requests/projects/metrics/dashboards/builder_spec.rb
index c929beaed70..8af2d1f1d25 100644
--- a/spec/requests/projects/metrics/dashboards/builder_spec.rb
+++ b/spec/requests/projects/metrics/dashboards/builder_spec.rb
@@ -49,6 +49,10 @@ RSpec.describe 'Projects::Metrics::Dashboards::BuilderController', feature_categ
end
describe 'POST /:namespace/:project/-/metrics/dashboards/builder' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: false)
+ end
+
context 'as anonymous user' do
it 'redirects user to sign in page' do
send_request
@@ -102,6 +106,18 @@ RSpec.describe 'Projects::Metrics::Dashboards::BuilderController', feature_categ
expect(json_response['message']).to eq('Invalid configuration format')
end
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'returns not found' do
+ send_request
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
end
end
diff --git a/spec/requests/projects/metrics_dashboard_spec.rb b/spec/requests/projects/metrics_dashboard_spec.rb
index 01925f8345b..d0181275927 100644
--- a/spec/requests/projects/metrics_dashboard_spec.rb
+++ b/spec/requests/projects/metrics_dashboard_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe 'Projects::MetricsDashboardController', feature_category: :metric
before do
project.add_developer(user)
login_as(user)
+ stub_feature_flags(remove_monitor_metrics: false)
end
describe 'GET /:namespace/:project/-/metrics' do
@@ -37,6 +38,17 @@ RSpec.describe 'Projects::MetricsDashboardController', feature_category: :metric
expect(response).to redirect_to(dashboard_route(params.merge(environment: environment.id)))
end
+ context 'with remove_monitor_metrics returning true' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'renders 404 page' do
+ send_request
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
context 'with anonymous user and public dashboard visibility' do
let(:anonymous_user) { create(:user) }
let(:project) do
diff --git a/spec/requests/projects/ml/candidates_controller_spec.rb b/spec/requests/projects/ml/candidates_controller_spec.rb
index d3f9d92bc44..78c8e99e3f3 100644
--- a/spec/requests/projects/ml/candidates_controller_spec.rb
+++ b/spec/requests/projects/ml/candidates_controller_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
let_it_be(:experiment) { create(:ml_experiments, project: project, user: user) }
- let_it_be(:candidate) { create(:ml_candidates, experiment: experiment, user: user) }
+ let_it_be(:candidate) { create(:ml_candidates, experiment: experiment, user: user, project: project) }
let(:ff_value) { true }
let(:candidate_iid) { candidate.iid }
@@ -18,19 +18,29 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do
sign_in(user)
end
+ shared_examples 'renders 404' do
+ it 'renders 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ shared_examples '404 if candidate does not exist' do
+ context 'when experiment does not exist' do
+ let(:candidate_iid) { non_existing_record_id }
+
+ it_behaves_like 'renders 404'
+ end
+ end
+
shared_examples '404 if feature flag disabled' do
context 'when :ml_experiment_tracking disabled' do
let(:ff_value) { false }
- it 'is 404' do
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ it_behaves_like 'renders 404'
end
end
describe 'GET show' do
- let(:params) { basic_params.merge(id: experiment.iid) }
-
before do
show_candidate
end
@@ -48,20 +58,39 @@ RSpec.describe Projects::Ml::CandidatesController, feature_category: :mlops do
expect { show_candidate }.not_to exceed_all_query_limit(control_count)
end
- context 'when candidate does not exist' do
- let(:candidate_iid) { non_existing_record_id.to_s }
+ it_behaves_like '404 if candidate does not exist'
+ it_behaves_like '404 if feature flag disabled'
+ end
+
+ describe 'DELETE #destroy' do
+ let_it_be(:candidate_for_deletion) do
+ create(:ml_candidates, project: project, experiment: experiment, user: user)
+ end
+
+ let(:candidate_iid) { candidate_for_deletion.iid }
- it 'returns 404' do
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ before do
+ destroy_candidate
end
+ it 'deletes the experiment', :aggregate_failures do
+ expect(response).to have_gitlab_http_status(:found)
+ expect(flash[:notice]).to eq('Candidate removed')
+ expect(response).to redirect_to("/#{project.full_path}/-/ml/experiments/#{experiment.iid}")
+ expect { Ml::Candidate.find(id: candidate_for_deletion.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it_behaves_like '404 if candidate does not exist'
it_behaves_like '404 if feature flag disabled'
end
private
def show_candidate
- get project_ml_candidate_path(project, candidate_iid)
+ get project_ml_candidate_path(project, iid: candidate_iid)
+ end
+
+ def destroy_candidate
+ delete project_ml_candidate_path(project, candidate_iid)
end
end
diff --git a/spec/requests/projects/ml/experiments_controller_spec.rb b/spec/requests/projects/ml/experiments_controller_spec.rb
index 9b071efc1f1..5a8496a250a 100644
--- a/spec/requests/projects/ml/experiments_controller_spec.rb
+++ b/spec/requests/projects/ml/experiments_controller_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
let(:ff_value) { true }
let(:project) { project_with_feature }
let(:basic_params) { { namespace_id: project.namespace.to_param, project_id: project } }
+ let(:experiment_iid) { experiment.iid }
before do
stub_feature_flags(ml_experiment_tracking: false)
@@ -27,13 +28,25 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
sign_in(user)
end
+ shared_examples 'renders 404' do
+ it 'renders 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ shared_examples '404 if experiment does not exist' do
+ context 'when experiment does not exist' do
+ let(:experiment_iid) { non_existing_record_id }
+
+ it_behaves_like 'renders 404'
+ end
+ end
+
shared_examples '404 if feature flag disabled' do
context 'when :ml_experiment_tracking disabled' do
let(:ff_value) { false }
- it 'is 404' do
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ it_behaves_like 'renders 404'
end
end
@@ -109,119 +122,184 @@ RSpec.describe Projects::Ml::ExperimentsController, feature_category: :mlops do
end
describe 'GET show' do
- let(:params) { basic_params.merge(id: experiment.iid) }
+ describe 'html' do
+ it 'renders the template' do
+ show_experiment
+
+ expect(response).to render_template('projects/ml/experiments/show')
+ end
- it 'renders the template' do
- show_experiment
+ describe 'pagination' do
+ let_it_be(:candidates) do
+ create_list(:ml_candidates, 5, experiment: experiment).tap do |c|
+ c.first.metrics.create!(name: 'metric1', value: 0.3)
+ c[1].metrics.create!(name: 'metric1', value: 0.2)
+ c.last.metrics.create!(name: 'metric1', value: 0.6)
+ end
+ end
- expect(response).to render_template('projects/ml/experiments/show')
- end
+ let(:params) { basic_params.merge(id: experiment.iid) }
- describe 'pagination' do
- let_it_be(:candidates) do
- create_list(:ml_candidates, 5, experiment: experiment).tap do |c|
- c.first.metrics.create!(name: 'metric1', value: 0.3)
- c[1].metrics.create!(name: 'metric1', value: 0.2)
- c.last.metrics.create!(name: 'metric1', value: 0.6)
+ before do
+ stub_const("Projects::Ml::ExperimentsController::MAX_CANDIDATES_PER_PAGE", 2)
+
+ show_experiment
end
- end
- let(:params) { basic_params.merge(id: experiment.iid) }
+ it 'fetches only MAX_CANDIDATES_PER_PAGE candidates' do
+ expect(assigns(:candidates).size).to eq(2)
+ end
- before do
- stub_const("Projects::Ml::ExperimentsController::MAX_CANDIDATES_PER_PAGE", 2)
+ it 'paginates' do
+ received = assigns(:page_info)
- show_experiment
- end
+ expect(received).to include({
+ has_next_page: true,
+ has_previous_page: false,
+ start_cursor: nil
+ })
+ end
- it 'fetches only MAX_CANDIDATES_PER_PAGE candidates' do
- expect(assigns(:candidates).size).to eq(2)
- end
+ context 'when order by metric' do
+ let(:params) do
+ {
+ order_by: "metric1",
+ order_by_type: "metric",
+ sort: "desc"
+ }
+ end
+
+ it 'paginates', :aggregate_failures do
+ page = assigns(:candidates)
+
+ expect(page.first).to eq(candidates.last)
+ expect(page.last).to eq(candidates.first)
- it 'paginates' do
- received = assigns(:page_info)
+ new_params = params.merge(cursor: assigns(:page_info)[:end_cursor])
- expect(received).to include({
- has_next_page: true,
- has_previous_page: false,
- start_cursor: nil
- })
+ show_experiment(new_params: new_params)
+
+ new_page = assigns(:candidates)
+
+ expect(new_page.first).to eq(candidates[1])
+ end
+ end
end
- context 'when order by metric' do
+ describe 'search' do
let(:params) do
- {
- order_by: "metric1",
- order_by_type: "metric",
- sort: "desc"
- }
+ basic_params.merge(
+ name: 'some_name',
+ orderBy: 'name',
+ orderByType: 'metric',
+ sort: 'asc',
+ invalid: 'invalid'
+ )
end
- it 'paginates', :aggregate_failures do
- page = assigns(:candidates)
-
- expect(page.first).to eq(candidates.last)
- expect(page.last).to eq(candidates.first)
+ it 'formats and filters the parameters' do
+ expect(Projects::Ml::CandidateFinder).to receive(:new).and_call_original do |exp, params|
+ expect(params.to_h).to include({
+ name: 'some_name',
+ order_by: 'name',
+ order_by_type: 'metric',
+ sort: 'asc'
+ })
+ end
+
+ show_experiment
+ end
+ end
- new_params = params.merge(cursor: assigns(:page_info)[:end_cursor])
+ it 'does not perform N+1 sql queries' do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { show_experiment }
- show_experiment(new_params)
+ create_list(:ml_candidates, 2, :with_metrics_and_params, experiment: experiment)
- new_page = assigns(:candidates)
+ expect { show_experiment }.not_to exceed_all_query_limit(control_count)
+ end
- expect(new_page.first).to eq(candidates[1])
+ describe '404' do
+ before do
+ show_experiment
end
+
+ it_behaves_like '404 if experiment does not exist'
+ it_behaves_like '404 if feature flag disabled'
end
end
- describe 'search' do
- let(:params) do
- basic_params.merge(
- id: experiment.iid,
- name: 'some_name',
- orderBy: 'name',
- orderByType: 'metric',
- sort: 'asc',
- invalid: 'invalid'
- )
- end
-
- it 'formats and filters the parameters' do
- expect(Projects::Ml::CandidateFinder).to receive(:new).and_call_original do |exp, params|
- expect(params.to_h).to include({
- name: 'some_name',
- order_by: 'name',
- order_by_type: 'metric',
- sort: 'asc'
- })
+ describe 'csv' do
+ it 'responds with :ok', :aggregate_failures do
+ show_experiment_csv
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['Content-Type']).to eq('text/csv; charset=utf-8')
+ end
+
+ it 'calls the presenter' do
+ allow(::Ml::CandidatesCsvPresenter).to receive(:new).and_call_original
+
+ show_experiment_csv
+ end
+
+ it 'does not perform N+1 sql queries' do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { show_experiment_csv }
+
+ create_list(:ml_candidates, 2, :with_metrics_and_params, experiment: experiment)
+
+ expect { show_experiment_csv }.not_to exceed_all_query_limit(control_count)
+ end
+
+ describe '404' do
+ before do
+ show_experiment_csv
end
- show_experiment
+ it_behaves_like '404 if experiment does not exist'
+ it_behaves_like '404 if feature flag disabled'
end
end
+ end
- it 'does not perform N+1 sql queries' do
- control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { show_experiment }
+ describe 'DELETE #destroy' do
+ let_it_be(:experiment_for_deletion) do
+ create(:ml_experiments, project: project_with_feature, user: user).tap do |e|
+ create(:ml_candidates, experiment: e, user: user)
+ end
+ end
+
+ let_it_be(:candidate_for_deletion) { experiment_for_deletion.candidates.first }
- create_list(:ml_candidates, 2, :with_metrics_and_params, experiment: experiment)
+ let(:params) { basic_params.merge(id: experiment.iid) }
- expect { show_experiment }.not_to exceed_all_query_limit(control_count)
+ before do
+ destroy_experiment
end
- it_behaves_like '404 if feature flag disabled' do
- before do
- show_experiment
- end
+ it 'deletes the experiment' do
+ expect { experiment.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
+
+ it_behaves_like '404 if experiment does not exist'
+ it_behaves_like '404 if feature flag disabled'
end
private
- def show_experiment(new_params = nil)
- get project_ml_experiment_path(project, experiment.iid), params: new_params || params
+ def show_experiment(new_params: nil, format: :html)
+ get project_ml_experiment_path(project, experiment_iid, format: format), params: new_params || params
+ end
+
+ def show_experiment_csv
+ show_experiment(format: :csv)
end
def list_experiments(new_params = nil)
get project_ml_experiments_path(project), params: new_params || params
end
+
+ def destroy_experiment
+ delete project_ml_experiment_path(project, experiment_iid), params: params
+ end
end
diff --git a/spec/requests/projects/pipelines_controller_spec.rb b/spec/requests/projects/pipelines_controller_spec.rb
index 73e002b63b1..7bdb66755db 100644
--- a/spec/requests/projects/pipelines_controller_spec.rb
+++ b/spec/requests/projects/pipelines_controller_spec.rb
@@ -23,18 +23,25 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
it 'does not execute N+1 queries' do
get_pipelines_index
- control_count = ActiveRecord::QueryRecorder.new do
+ create_pipelines
+
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
get_pipelines_index
end.count
- %w[pending running success failed canceled].each do |status|
- create(:ci_pipeline, project: project, status: status)
- end
+ create_pipelines
# There appears to be one extra query for Pipelines#has_warnings? for some reason
- expect { get_pipelines_index }.not_to exceed_query_limit(control_count + 1)
+ expect { get_pipelines_index }.not_to exceed_all_query_limit(control_count + 1)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['pipelines'].count).to eq 6
+ expect(json_response['pipelines'].count).to eq(11)
+ end
+
+ def create_pipelines
+ %w[pending running success failed canceled].each do |status|
+ pipeline = create(:ci_pipeline, project: project, status: status)
+ create(:ci_build, :failed, pipeline: pipeline)
+ end
end
def get_pipelines_index
@@ -49,13 +56,21 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
it 'does not execute N+1 queries' do
request_build_stage
- control_count = ActiveRecord::QueryRecorder.new do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
request_build_stage
end.count
create(:ci_build, pipeline: pipeline, stage: 'build')
- expect { request_build_stage }.not_to exceed_query_limit(control_count)
+ 2.times do |i|
+ create(:ci_build,
+ name: "test retryable #{i}",
+ pipeline: pipeline,
+ stage: 'build',
+ status: :failed)
+ end
+
+ expect { request_build_stage }.not_to exceed_all_query_limit(control_count)
expect(response).to have_gitlab_http_status(:ok)
end
@@ -66,13 +81,14 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
request_build_stage(retried: true)
- control_count = ActiveRecord::QueryRecorder.new do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
request_build_stage(retried: true)
end.count
create(:ci_build, :retried, :failed, pipeline: pipeline, stage: 'build')
+ create(:ci_build, :failed, pipeline: pipeline, stage: 'build')
- expect { request_build_stage(retried: true) }.not_to exceed_query_limit(control_count)
+ expect { request_build_stage(retried: true) }.not_to exceed_all_query_limit(control_count)
expect(response).to have_gitlab_http_status(:ok)
end
diff --git a/spec/requests/projects/settings/access_tokens_controller_spec.rb b/spec/requests/projects/settings/access_tokens_controller_spec.rb
index defb35fd496..666dc42bcab 100644
--- a/spec/requests/projects/settings/access_tokens_controller_spec.rb
+++ b/spec/requests/projects/settings/access_tokens_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::Settings::AccessTokensController, feature_category: :authentication_and_authorization do
+RSpec.describe Projects::Settings::AccessTokensController, feature_category: :system_access do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:resource) { create(:project, group: group) }
diff --git a/spec/requests/projects/uploads_spec.rb b/spec/requests/projects/uploads_spec.rb
index aec2636b69c..a591f479763 100644
--- a/spec/requests/projects/uploads_spec.rb
+++ b/spec/requests/projects/uploads_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'File uploads', feature_category: :not_owned do
+RSpec.describe 'File uploads', feature_category: :shared do
include WorkhorseHelpers
let(:project) { create(:project, :public, :repository) }
diff --git a/spec/requests/projects/usage_quotas_spec.rb b/spec/requests/projects/usage_quotas_spec.rb
index 60ab64c30c3..33b206c8dc0 100644
--- a/spec/requests/projects/usage_quotas_spec.rb
+++ b/spec/requests/projects/usage_quotas_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Project Usage Quotas', feature_category: :subscription_cost_management do
+RSpec.describe 'Project Usage Quotas', feature_category: :consumables_cost_management do
let_it_be(:project) { create(:project) }
let_it_be(:role) { :maintainer }
let_it_be(:user) { create(:user) }
diff --git a/spec/requests/projects/wikis_controller_spec.rb b/spec/requests/projects/wikis_controller_spec.rb
new file mode 100644
index 00000000000..3c434b36b21
--- /dev/null
+++ b/spec/requests/projects/wikis_controller_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::WikisController, feature_category: :wiki do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
+ let_it_be(:project_wiki) { create(:project_wiki, project: project, user: user) }
+ let_it_be(:wiki_page) do
+ create(:wiki_page,
+ wiki: project_wiki,
+ title: 'home', content: "Look at this [image](#{path})\n\n ![alt text](#{path})")
+ end
+
+ let_it_be(:csp_nonce) { 'just=some=noncense' }
+
+ before do
+ sign_in(user)
+
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:content_security_policy_nonce).and_return(csp_nonce)
+ end
+ end
+
+ shared_examples 'embed.diagrams.net frame-src directive' do
+ it 'adds drawio frame-src directive to the Content Security Policy header' do
+ frame_src = response.headers['Content-Security-Policy'].split(';')
+ .map(&:strip)
+ .find { |entry| entry.starts_with?('frame-src') }
+
+ expect(frame_src).to include('https://embed.diagrams.net')
+ end
+ end
+
+ describe 'CSP policy' do
+ describe '#new' do
+ before do
+ get wiki_path(project_wiki, action: :new)
+ end
+
+ it_behaves_like 'embed.diagrams.net frame-src directive'
+ end
+
+ describe '#edit' do
+ before do
+ get wiki_page_path(project_wiki, wiki_page, action: 'edit')
+ end
+
+ it_behaves_like 'embed.diagrams.net frame-src directive'
+ end
+
+ describe '#create' do
+ before do
+ # Creating a page with an invalid title to render edit page
+ post wiki_path(project_wiki, action: 'create'), params: { wiki: { title: 'home' } }
+ end
+
+ it_behaves_like 'embed.diagrams.net frame-src directive'
+ end
+
+ describe '#update' do
+ before do
+ # Setting an invalid page title to render edit page
+ put wiki_page_path(project_wiki, wiki_page), params: { wiki: { title: '' } }
+ end
+
+ it_behaves_like 'embed.diagrams.net frame-src directive'
+ end
+ end
+end
diff --git a/spec/requests/projects/work_items_spec.rb b/spec/requests/projects/work_items_spec.rb
index 056416d380d..c02f76d2c65 100644
--- a/spec/requests/projects/work_items_spec.rb
+++ b/spec/requests/projects/work_items_spec.rb
@@ -3,22 +3,192 @@
require 'spec_helper'
RSpec.describe 'Work Items', feature_category: :team_planning do
+ include WorkhorseHelpers
+
+ include_context 'workhorse headers'
+
let_it_be(:work_item) { create(:work_item) }
- let_it_be(:developer) { create(:user) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ let(:file) { fixture_file_upload("spec/fixtures/#{filename}") }
before_all do
- work_item.project.add_developer(developer)
+ work_item.project.add_developer(current_user)
+ end
+
+ shared_examples 'response with 404 status' do
+ it 'returns 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ shared_examples 'safely handles uploaded files' do
+ it 'ensures the upload is handled safely', :aggregate_failures do
+ allow(Gitlab::Utils).to receive(:check_path_traversal!).and_call_original
+ expect(Gitlab::Utils).to receive(:check_path_traversal!).with(filename).at_least(:once)
+ expect(FileUploader).not_to receive(:cache)
+
+ subject
+ end
end
describe 'GET /:namespace/:project/work_items/:id' do
before do
- sign_in(developer)
+ sign_in(current_user)
end
it 'renders index' do
- get project_work_items_url(work_item.project, work_items_path: work_item.id)
+ get project_work_items_url(work_item.project, work_items_path: work_item.iid)
expect(response).to have_gitlab_http_status(:ok)
end
end
+
+ describe 'POST /:namespace/:project/work_items/import_csv' do
+ let(:filename) { 'work_items_valid_types.csv' }
+ let(:params) { { namespace_id: project.namespace.id, path: 'test' } }
+
+ subject { upload_file(file, workhorse_headers, params) }
+
+ shared_examples 'handles authorisation' do
+ context 'when unauthorized' do
+ context 'with non-member' do
+ let_it_be(:current_user) { create(:user) }
+
+ before do
+ sign_in(current_user)
+ end
+
+ it 'responds with error' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with anonymous user' do
+ it 'responds with error' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response).to be_redirect
+ end
+ end
+ end
+
+ context 'when authorized' do
+ before do
+ sign_in(current_user)
+ project.add_reporter(current_user)
+ end
+
+ context 'when import/export work items feature is available and member is a reporter' do
+ shared_examples 'response with success status' do
+ it 'returns 200 status and success message' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(json_response).to eq(
+ 'message' => "Your work items are being imported. Once finished, you'll receive a confirmation email.")
+ end
+ end
+
+ it_behaves_like 'response with success status'
+ it_behaves_like 'safely handles uploaded files'
+
+ it 'shows error when upload fails' do
+ expect_next_instance_of(UploadService) do |upload_service|
+ expect(upload_service).to receive(:execute).and_return(nil)
+ end
+
+ subject
+
+ expect(json_response).to eq('errors' => 'File upload error.')
+ end
+
+ context 'when file extension is not csv' do
+ let(:filename) { 'sample_doc.md' }
+
+ it 'returns error message' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response).to eq(
+ 'errors' => "The uploaded file was invalid. Supported file extensions are .csv.")
+ end
+ end
+ end
+
+ context 'when work items import/export feature is not available' do
+ before do
+ stub_feature_flags(import_export_work_items_csv: false)
+ end
+
+ it_behaves_like 'response with 404 status'
+ end
+ end
+ end
+
+ context 'with public project' do
+ let_it_be(:project) { create(:project, :public) }
+
+ it_behaves_like 'handles authorisation'
+ end
+
+ context 'with private project' do
+ it_behaves_like 'handles authorisation'
+ end
+
+ def upload_file(file, headers = {}, params = {})
+ workhorse_finalize(
+ import_csv_project_work_items_path(project),
+ method: :post,
+ file_key: :file,
+ params: params.merge(file: file),
+ headers: headers,
+ send_rewritten_field: true
+ )
+ end
+ end
+
+ describe 'POST #authorize' do
+ subject do
+ post import_csv_authorize_project_work_items_path(project),
+ headers: workhorse_headers
+ end
+
+ before do
+ sign_in(current_user)
+ end
+
+ context 'with authorized user' do
+ before do
+ project.add_reporter(current_user)
+ end
+
+ context 'when work items import/export feature is enabled' do
+ let(:user) { current_user }
+
+ it_behaves_like 'handle uploads authorize request' do
+ let(:uploader_class) { FileUploader }
+ let(:maximum_size) { Gitlab::CurrentSettings.max_attachment_size.megabytes }
+ end
+ end
+
+ context 'when work items import/export feature is disabled' do
+ before do
+ stub_feature_flags(import_export_work_items_csv: false)
+ end
+
+ it_behaves_like 'response with 404 status'
+ end
+ end
+
+ context 'with unauthorized user' do
+ it_behaves_like 'response with 404 status'
+ end
+ end
end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 91595f7826a..0dd8a15c3a4 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_caching,
-feature_category: :authentication_and_authorization do
+feature_category: :system_access do
include RackAttackSpecHelpers
include SessionHelpers
diff --git a/spec/requests/registrations_controller_spec.rb b/spec/requests/registrations_controller_spec.rb
new file mode 100644
index 00000000000..8b857046a4d
--- /dev/null
+++ b/spec/requests/registrations_controller_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe RegistrationsController, type: :request, feature_category: :system_access do
+ describe 'POST #create' do
+ let_it_be(:user_attrs) { build_stubbed(:user).slice(:first_name, :last_name, :username, :email, :password) }
+
+ subject(:create_user) { post user_registration_path, params: { user: user_attrs } }
+
+ context 'when email confirmation is required' do
+ before do
+ stub_application_setting_enum('email_confirmation_setting', 'hard')
+ stub_application_setting(require_admin_approval_after_user_signup: false)
+ end
+
+ it 'redirects to the `users_almost_there_path`', unless: Gitlab.ee? do
+ create_user
+
+ expect(response).to redirect_to(users_almost_there_path(email: user_attrs[:email]))
+ end
+ end
+ end
+end
diff --git a/spec/requests/sandbox_controller_spec.rb b/spec/requests/sandbox_controller_spec.rb
index 77913065380..26a7422680c 100644
--- a/spec/requests/sandbox_controller_spec.rb
+++ b/spec/requests/sandbox_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SandboxController, feature_category: :not_owned do
+RSpec.describe SandboxController, feature_category: :shared do
describe 'GET #mermaid' do
it 'renders page without template' do
get sandbox_mermaid_path
diff --git a/spec/requests/search_controller_spec.rb b/spec/requests/search_controller_spec.rb
index 98dda75a2b0..f2d4e288ddc 100644
--- a/spec/requests/search_controller_spec.rb
+++ b/spec/requests/search_controller_spec.rb
@@ -66,13 +66,9 @@ RSpec.describe SearchController, type: :request, feature_category: :global_searc
let(:creation_args) { { name: 'project' } }
let(:params) { { search: 'project', scope: 'projects' } }
# some N+1 queries still exist
- # each project requires 3 extra queries
- # - one count for forks
- # - one count for open MRs
- # - one count for open Issues
- # there are 4 additional queries run for the logged in user:
- # (1) user preferences, (1) user statuses, (1) user details, (1) users
- let(:threshold) { 17 }
+ # 1 for users
+ # 1 for root ancestor for each project
+ let(:threshold) { 7 }
it_behaves_like 'an efficient database result'
end
diff --git a/spec/requests/self_monitoring_project_spec.rb b/spec/requests/self_monitoring_project_spec.rb
deleted file mode 100644
index ce4dd10a52d..00000000000
--- a/spec/requests/self_monitoring_project_spec.rb
+++ /dev/null
@@ -1,213 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Self-Monitoring project requests', feature_category: :projects do
- let(:admin) { create(:admin) }
-
- describe 'POST #create_self_monitoring_project' do
- let(:worker_class) { SelfMonitoringProjectCreateWorker }
-
- subject { post create_self_monitoring_project_admin_application_settings_path }
-
- it_behaves_like 'not accessible to non-admin users'
-
- context 'with admin user', :enable_admin_mode do
- before do
- login_as(admin)
- end
-
- context 'when the self-monitoring project is created' do
- let(:status_api) { status_create_self_monitoring_project_admin_application_settings_path }
-
- it_behaves_like 'triggers async worker, returns sidekiq job_id with response accepted'
- end
- end
- end
-
- describe 'GET #status_create_self_monitoring_project' do
- let(:worker_class) { SelfMonitoringProjectCreateWorker }
- let(:job_id) { 'job_id' }
-
- subject do
- get status_create_self_monitoring_project_admin_application_settings_path,
- params: { job_id: job_id }
- end
-
- it_behaves_like 'not accessible to non-admin users'
-
- context 'with admin user', :enable_admin_mode do
- before do
- login_as(admin)
- end
-
- context 'when the self-monitoring project is being created' do
- it_behaves_like 'handles invalid job_id'
-
- context 'when job is in progress' do
- before do
- allow(worker_class).to receive(:in_progress?)
- .with(job_id)
- .and_return(true)
- end
-
- it_behaves_like 'sets polling header and returns accepted' do
- let(:in_progress_message) { 'Job to create self-monitoring project is in progress' }
- end
- end
-
- context 'when self-monitoring project and job do not exist' do
- let(:job_id) { nil }
-
- it 'returns bad_request' do
- create(:application_setting)
-
- subject
-
- aggregate_failures do
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response).to eq(
- 'message' => 'Self-monitoring project does not exist. Please check logs ' \
- 'for any error messages'
- )
- end
- end
- end
-
- context 'when self-monitoring project exists' do
- let(:project) { create(:project) }
-
- before do
- create(:application_setting, self_monitoring_project_id: project.id)
- end
-
- it 'does not need job_id' do
- get status_create_self_monitoring_project_admin_application_settings_path
-
- aggregate_failures do
- expect(response).to have_gitlab_http_status(:success)
- expect(json_response).to eq(
- 'project_id' => project.id,
- 'project_full_path' => project.full_path
- )
- end
- end
-
- it 'returns success with job_id' do
- subject
-
- aggregate_failures do
- expect(response).to have_gitlab_http_status(:success)
- expect(json_response).to eq(
- 'project_id' => project.id,
- 'project_full_path' => project.full_path
- )
- end
- end
- end
- end
- end
- end
-
- describe 'DELETE #delete_self_monitoring_project' do
- let(:worker_class) { SelfMonitoringProjectDeleteWorker }
-
- subject { delete delete_self_monitoring_project_admin_application_settings_path }
-
- it_behaves_like 'not accessible to non-admin users'
-
- context 'with admin user', :enable_admin_mode do
- before do
- login_as(admin)
- end
-
- context 'when the self-monitoring project is deleted' do
- let(:status_api) { status_delete_self_monitoring_project_admin_application_settings_path }
-
- it_behaves_like 'triggers async worker, returns sidekiq job_id with response accepted'
- end
- end
- end
-
- describe 'GET #status_delete_self_monitoring_project' do
- let(:worker_class) { SelfMonitoringProjectDeleteWorker }
- let(:job_id) { 'job_id' }
-
- subject do
- get status_delete_self_monitoring_project_admin_application_settings_path,
- params: { job_id: job_id }
- end
-
- it_behaves_like 'not accessible to non-admin users'
-
- context 'with admin user', :enable_admin_mode do
- before do
- login_as(admin)
- end
-
- context 'when the self-monitoring project is being deleted' do
- it_behaves_like 'handles invalid job_id'
-
- context 'when job is in progress' do
- before do
- allow(worker_class).to receive(:in_progress?)
- .with(job_id)
- .and_return(true)
-
- stub_application_setting(self_monitoring_project_id: 1)
- end
-
- it_behaves_like 'sets polling header and returns accepted' do
- let(:in_progress_message) { 'Job to delete self-monitoring project is in progress' }
- end
- end
-
- context 'when self-monitoring project exists and job does not exist' do
- before do
- create(:application_setting, self_monitoring_project_id: create(:project).id)
- end
-
- it 'returns bad_request' do
- subject
-
- aggregate_failures do
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response).to eq(
- 'message' => 'Self-monitoring project was not deleted. Please check logs ' \
- 'for any error messages'
- )
- end
- end
- end
-
- context 'when self-monitoring project does not exist' do
- before do
- create(:application_setting)
- end
-
- it 'does not need job_id' do
- get status_delete_self_monitoring_project_admin_application_settings_path
-
- aggregate_failures do
- expect(response).to have_gitlab_http_status(:success)
- expect(json_response).to eq(
- 'message' => 'Self-monitoring project has been successfully deleted'
- )
- end
- end
-
- it 'returns success with job_id' do
- subject
-
- aggregate_failures do
- expect(response).to have_gitlab_http_status(:success)
- expect(json_response).to eq(
- 'message' => 'Self-monitoring project has been successfully deleted'
- )
- end
- end
- end
- end
- end
- end
-end
diff --git a/spec/requests/sessions_spec.rb b/spec/requests/sessions_spec.rb
index 7b3fd23980a..3bff9555834 100644
--- a/spec/requests/sessions_spec.rb
+++ b/spec/requests/sessions_spec.rb
@@ -2,7 +2,9 @@
require 'spec_helper'
-RSpec.describe 'Sessions', feature_category: :authentication_and_authorization do
+RSpec.describe 'Sessions', feature_category: :system_access do
+ include SessionHelpers
+
context 'authentication', :allow_forgery_protection do
let(:user) { create(:user) }
@@ -14,4 +16,48 @@ RSpec.describe 'Sessions', feature_category: :authentication_and_authorization d
expect(response).to redirect_to(new_user_session_path)
end
end
+
+ describe 'about_gitlab_active_user' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(true)
+ end
+
+ let(:user) { create(:user) }
+
+ context 'when user signs in' do
+ it 'sets marketing cookie' do
+ post user_session_path(user: { login: user.username, password: user.password })
+ expect(response.cookies['about_gitlab_active_user']).to be_present
+ end
+ end
+
+ context 'when user uses remember_me' do
+ it 'sets marketing cookie' do
+ post user_session_path(user: { login: user.username, password: user.password, remember_me: true })
+ expect(response.cookies['about_gitlab_active_user']).to be_present
+ end
+ end
+
+ context 'when user signs out' do
+ before do
+ post user_session_path(user: { login: user.username, password: user.password })
+ end
+
+ it 'deletes marketing cookie' do
+ post(destroy_user_session_path)
+ expect(response.cookies['about_gitlab_active_user']).to be_nil
+ end
+ end
+
+ context 'when user is not using GitLab SaaS' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it 'does not set marketing cookie' do
+ post user_session_path(user: { login: user.username, password: user.password })
+ expect(response.cookies['about_gitlab_active_user']).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/requests/time_tracking/timelogs_controller_spec.rb b/spec/requests/time_tracking/timelogs_controller_spec.rb
new file mode 100644
index 00000000000..68eecf9b137
--- /dev/null
+++ b/spec/requests/time_tracking/timelogs_controller_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe TimeTracking::TimelogsController, feature_category: :team_planning do
+ let_it_be(:user) { create(:user) }
+
+ describe 'GET #index' do
+ subject { get timelogs_path }
+
+ context 'when user is not logged in' do
+ it 'responds with a redirect to the login page' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:redirect)
+ end
+ end
+
+ context 'when user is logged in' do
+ before do
+ sign_in(user)
+ end
+
+ context 'when global_time_tracking_report FF is enabled' do
+ it 'responds with the global time tracking page', :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:index)
+ end
+ end
+
+ context 'when global_time_tracking_report FF is disable' do
+ before do
+ stub_feature_flags(global_time_tracking_report: false)
+ end
+
+ it 'returns a 404 page' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/users/pins_spec.rb b/spec/requests/users/pins_spec.rb
new file mode 100644
index 00000000000..9a32d7e9d76
--- /dev/null
+++ b/spec/requests/users/pins_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Pinning navigation menu items', feature_category: :navigation do
+ let(:user) { create(:user) }
+ let(:menu_item_ids) { %w[item4 item7] }
+ let(:other_panel_data) { { 'group' => ['some_item_id'] } }
+
+ before do
+ user.update!(pinned_nav_items: other_panel_data)
+ sign_in(user)
+ end
+
+ describe 'PUT /-/users/pins' do
+ before do
+ put pins_path, params: params, headers: { 'ACCEPT' => 'application/json' }
+ end
+
+ context 'with valid params' do
+ let(:panel) { 'project' }
+ let(:params) { { menu_item_ids: menu_item_ids, panel: panel } }
+
+ it 'saves the menu_item_ids for the correct panel' do
+ expect(user.pinned_nav_items).to include(panel => menu_item_ids)
+ end
+
+ it 'does not change menu_item_ids of other panels' do
+ expect(user.pinned_nav_items).to include(other_panel_data)
+ end
+
+ it 'responds OK' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with invalid params' do
+ shared_examples 'unchanged data and error response' do
+ it 'does not modify existing panel data' do
+ expect(user.reload.pinned_nav_items).to eq(other_panel_data)
+ end
+
+ it 'responds with error' do
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'when panel name is unknown' do
+ let(:params) { { menu_item_ids: menu_item_ids, panel: 'something_else' } }
+
+ it_behaves_like 'unchanged data and error response'
+ end
+
+ context 'when menu_item_ids is not array of strings' do
+ let(:params) { { menu_item_ids: 'not_an_array', panel: 'project' } }
+
+ it_behaves_like 'unchanged data and error response'
+ end
+
+ context 'when params are not permitted' do
+ let(:params) { { random_param: 'random_value' } }
+
+ it_behaves_like 'unchanged data and error response'
+ end
+ end
+ end
+end
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index 11d8be24e06..c49dbb6a269 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -174,39 +174,95 @@ RSpec.describe UsersController, feature_category: :user_management do
end
context 'requested in json format' do
- let(:project) { create(:project) }
+ context 'when profile_tabs_vue feature flag is turned OFF' do
+ let(:project) { create(:project) }
- before do
- project.add_developer(user)
- Gitlab::DataBuilder::Push.build_sample(project, user)
+ before do
+ project.add_developer(user)
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ stub_feature_flags(profile_tabs_vue: false)
+ sign_in(user)
+ end
- sign_in(user)
- end
+ it 'loads events' do
+ get user_activity_url user.username, format: :json
- it 'loads events' do
- get user_activity_url user.username, format: :json
+ expect(response.media_type).to eq('application/json')
+ expect(Gitlab::Json.parse(response.body)['count']).to eq(1)
+ end
- expect(response.media_type).to eq('application/json')
- expect(Gitlab::Json.parse(response.body)['count']).to eq(1)
- end
+ it 'hides events if the user cannot read cross project' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
- it 'hides events if the user cannot read cross project' do
- allow(Ability).to receive(:allowed?).and_call_original
- expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
+ get user_activity_url user.username, format: :json
- get user_activity_url user.username, format: :json
+ expect(response.media_type).to eq('application/json')
+ expect(Gitlab::Json.parse(response.body)['count']).to eq(0)
+ end
- expect(response.media_type).to eq('application/json')
- expect(Gitlab::Json.parse(response.body)['count']).to eq(0)
+ it 'hides events if the user has a private profile' do
+ Gitlab::DataBuilder::Push.build_sample(project, private_user)
+
+ get user_activity_url private_user.username, format: :json
+
+ expect(response.media_type).to eq('application/json')
+ expect(Gitlab::Json.parse(response.body)['count']).to eq(0)
+ end
end
- it 'hides events if the user has a private profile' do
- Gitlab::DataBuilder::Push.build_sample(project, private_user)
+ context 'when profile_tabs_vue feature flag is turned ON' do
+ let(:project) { create(:project) }
+
+ before do
+ project.add_developer(user)
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ stub_feature_flags(profile_tabs_vue: true)
+ sign_in(user)
+ end
+
+ it 'loads events' do
+ get user_activity_url user.username, format: :json
- get user_activity_url private_user.username, format: :json
+ expect(response.media_type).to eq('application/json')
+ expect(Gitlab::Json.parse(response.body).count).to eq(1)
+ end
- expect(response.media_type).to eq('application/json')
- expect(Gitlab::Json.parse(response.body)['count']).to eq(0)
+ it 'hides events if the user cannot read cross project' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
+
+ get user_activity_url user.username, format: :json
+
+ expect(response.media_type).to eq('application/json')
+ expect(Gitlab::Json.parse(response.body).count).to eq(0)
+ end
+
+ it 'hides events if the user has a private profile' do
+ Gitlab::DataBuilder::Push.build_sample(project, private_user)
+
+ get user_activity_url private_user.username, format: :json
+
+ expect(response.media_type).to eq('application/json')
+ expect(Gitlab::Json.parse(response.body).count).to eq(0)
+ end
+
+ it 'hides events if the user has a private profile' do
+ project = create(:project, :private)
+ private_event_user = create(:user, include_private_contributions: true)
+ push_data = Gitlab::DataBuilder::Push.build_sample(project, private_event_user)
+ EventCreateService.new.push(project, private_event_user, push_data)
+
+ get user_activity_url private_event_user.username, format: :json
+
+ response_body = Gitlab::Json.parse(response.body)
+ event = response_body.first
+ expect(response.media_type).to eq('application/json')
+ expect(response_body.count).to eq(1)
+ expect(event).to include('created_at', 'author', 'action')
+ expect(event['action']).to eq('private')
+ expect(event).not_to include('ref', 'commit', 'target', 'resource_parent')
+ end
end
end
end
@@ -472,7 +528,7 @@ RSpec.describe UsersController, feature_category: :user_management do
get user_calendar_activities_url public_user.username
- expect(response.body).to include(project_work_items_path(project, work_item.iid, iid_path: true))
+ expect(response.body).to include(project_work_items_path(project, work_item.iid))
expect(response.body).to include(project_issue_path(project, issue))
end
@@ -714,6 +770,17 @@ RSpec.describe UsersController, feature_category: :user_management do
expect(response.body).to eq(expected_json)
end
end
+
+ context 'when a project has the same name as a desired username' do
+ let_it_be(:project) { create(:project, name: 'project-name') }
+
+ it 'returns JSON indicating a user by that username does not exist' do
+ get user_exists_url 'project-name'
+
+ expected_json = { exists: false }.to_json
+ expect(response.body).to eq(expected_json)
+ end
+ end
end
context 'when the rate limit has been reached' do
@@ -858,6 +925,35 @@ RSpec.describe UsersController, feature_category: :user_management do
expect(user).not_to be_following(public_user)
end
end
+
+ context 'when user or followee disabled following' do
+ before do
+ sign_in(user)
+ end
+
+ it 'alerts and not follow if user disabled following' do
+ user.enabled_following = false
+
+ post user_follow_url(username: public_user.username)
+ expect(response).to be_redirect
+
+ expected_message = format(_('Action not allowed.'))
+ expect(flash[:alert]).to eq(expected_message)
+ expect(user).not_to be_following(public_user)
+ end
+
+ it 'alerts and not follow if followee disabled following' do
+ public_user.enabled_following = false
+ public_user.save!
+
+ post user_follow_url(username: public_user.username)
+ expect(response).to be_redirect
+
+ expected_message = format(_('Action not allowed.'))
+ expect(flash[:alert]).to eq(expected_message)
+ expect(user).not_to be_following(public_user)
+ end
+ end
end
context 'token authentication' do
diff --git a/spec/requests/verifies_with_email_spec.rb b/spec/requests/verifies_with_email_spec.rb
index 8a6a7e717ff..6325ecc1184 100644
--- a/spec/requests/verifies_with_email_spec.rb
+++ b/spec/requests/verifies_with_email_spec.rb
@@ -42,7 +42,7 @@ feature_category: :user_management do
shared_examples_for 'two factor prompt or successful login' do
it 'shows the 2FA prompt when enabled or redirects to the root path' do
if user.two_factor_enabled?
- expect(response.body).to include('Two-factor authentication code')
+ expect(response.body).to include('Enter verification code')
else
expect(response).to redirect_to(root_path)
end
@@ -135,7 +135,7 @@ feature_category: :user_management do
describe 'verify_with_email' do
context 'when user is locked and a verification_user_id session variable exists' do
before do
- encrypted_token = Devise.token_generator.digest(User, :unlock_token, 'token')
+ encrypted_token = Devise.token_generator.digest(User, user.email, 'token')
user.update!(locked_at: Time.current, unlock_token: encrypted_token)
stub_session(verification_user_id: user.id)
end
diff --git a/spec/requests/web_ide/remote_ide_controller_spec.rb b/spec/requests/web_ide/remote_ide_controller_spec.rb
index 367c7527f10..9e9d3dfc703 100644
--- a/spec/requests/web_ide/remote_ide_controller_spec.rb
+++ b/spec/requests/web_ide/remote_ide_controller_spec.rb
@@ -72,7 +72,7 @@ RSpec.describe WebIde::RemoteIdeController, feature_category: :remote_developmen
end
it "updates the content security policy with the correct frame sources" do
- expect(find_csp_source('frame-src')).to include("https://*.vscode-cdn.net/")
+ expect(find_csp_source('frame-src')).to include("http://www.example.com/assets/webpack/", "https://*.vscode-cdn.net/")
end
end