diff options
93 files changed, 2474 insertions, 222 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index b08dbdf9888..b133ecbcf93 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -440,9 +440,6 @@ Graphql/Descriptions: - 'app/graphql/**/*' - 'ee/app/graphql/**/*' -RSpec/AnyInstanceOf: - Enabled: false - # Cops for upgrade to gitlab-styles 3.1.0 RSpec/ImplicitSubject: Enabled: false diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml index 290b776b33c..2ffbef850d0 100644 --- a/.rubocop_manual_todo.yml +++ b/.rubocop_manual_todo.yml @@ -1037,3 +1037,520 @@ Graphql/Descriptions: - 'ee/app/graphql/types/vulnerable_dependency_type.rb' - 'ee/app/graphql/types/vulnerable_package_type.rb' - 'ee/app/graphql/types/vulnerable_projects_by_grade_type.rb' + +# WIP: https://gitlab.com/gitlab-org/gitlab/-/issues/34997 +RSpec/AnyInstanceOf: + Exclude: + - 'ee/spec/controllers/admin/geo/nodes_controller_spec.rb' + - 'ee/spec/controllers/ee/groups_controller_spec.rb' + - 'ee/spec/controllers/groups/analytics/productivity_analytics_controller_spec.rb' + - 'ee/spec/controllers/groups/epics/notes_controller_spec.rb' + - 'ee/spec/controllers/groups/omniauth_callbacks_controller_spec.rb' + - 'ee/spec/controllers/oauth/geo_auth_controller_spec.rb' + - 'ee/spec/controllers/projects/environments_controller_spec.rb' + - 'ee/spec/controllers/projects/integrations/jira/issues_controller_spec.rb' + - 'ee/spec/controllers/projects/merge_requests_controller_spec.rb' + - 'ee/spec/controllers/projects/path_locks_controller_spec.rb' + - 'ee/spec/controllers/projects_controller_spec.rb' + - 'ee/spec/controllers/subscriptions_controller_spec.rb' + - 'ee/spec/controllers/trials_controller_spec.rb' + - 'ee/spec/features/admin/admin_audit_logs_spec.rb' + - 'ee/spec/features/admin/admin_reset_pipeline_minutes_spec.rb' + - 'ee/spec/features/admin/admin_users_spec.rb' + - 'ee/spec/features/admin/licenses/admin_views_license_spec.rb' + - 'ee/spec/features/boards/scoped_issue_board_spec.rb' + - 'ee/spec/features/ci_shared_runner_warnings_spec.rb' + - 'ee/spec/features/groups/group_settings_spec.rb' + - 'ee/spec/features/groups/navbar_spec.rb' + - 'ee/spec/features/groups/saml_providers_spec.rb' + - 'ee/spec/features/issues/form_spec.rb' + - 'ee/spec/features/merge_request/user_creates_merge_request_spec.rb' + - 'ee/spec/features/projects/new_project_spec.rb' + - 'ee/spec/features/projects/services/user_activates_jira_spec.rb' + - 'ee/spec/features/registrations/welcome_spec.rb' + - 'ee/spec/features/security/project/internal_access_spec.rb' + - 'ee/spec/features/security/project/private_access_spec.rb' + - 'ee/spec/features/security/project/public_access_spec.rb' + - 'ee/spec/features/trials/capture_lead_spec.rb' + - 'ee/spec/features/trials/select_namespace_spec.rb' + - 'ee/spec/features/users/login_spec.rb' + - 'ee/spec/graphql/mutations/dast_on_demand_scans/create_spec.rb' + - 'ee/spec/graphql/mutations/incident_management/oncall_schedule/create_spec.rb' + - 'ee/spec/graphql/mutations/incident_management/oncall_schedule/destroy_spec.rb' + - 'ee/spec/graphql/mutations/incident_management/oncall_schedule/update_spec.rb' + - 'ee/spec/helpers/application_helper_spec.rb' + - 'ee/spec/lib/ee/api/helpers_spec.rb' + - 'ee/spec/lib/ee/gitlab/auth/ldap/sync/group_spec.rb' + - 'ee/spec/lib/ee/gitlab/checks/push_rule_check_spec.rb' + - 'ee/spec/lib/ee/gitlab/checks/push_rules/commit_check_spec.rb' + - 'ee/spec/lib/gitlab/auth/group_saml/membership_enforcer_spec.rb' + - 'ee/spec/lib/gitlab/auth/ldap/access_spec.rb' + - 'ee/spec/lib/gitlab/ci/templates/Jobs/browser_performance_testing_gitlab_ci_yaml_spec.rb' + - 'ee/spec/lib/gitlab/ci/templates/Jobs/dast_default_branch_gitlab_ci_yaml_spec.rb' + - 'ee/spec/lib/gitlab/ci/templates/Jobs/load_performance_testing_gitlab_ci_yaml_spec.rb' + - 'ee/spec/lib/gitlab/ci/templates/Verify/browser_performance_testing_gitlab_ci_yaml_spec.rb' + - 'ee/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb' + - 'ee/spec/lib/gitlab/ci/templates/api_fuzzing_gitlab_ci_yaml_spec.rb' + - 'ee/spec/lib/gitlab/ci/templates/container_scanning_gitlab_ci_yaml_spec.rb' + - 'ee/spec/lib/gitlab/ci/templates/coverage_fuzzing_gitlab_ci_yaml_spec.rb' + - 'ee/spec/lib/gitlab/ci/templates/dast_gitlab_ci_yaml_spec.rb' + - 'ee/spec/lib/gitlab/ci/templates/dependency_scanning_gitlab_ci_yaml_spec.rb' + - 'ee/spec/lib/gitlab/ci/templates/license_scanning_gitlab_ci_yaml_spec.rb' + - 'ee/spec/lib/gitlab/ci/templates/sast_gitlab_ci_yaml_spec.rb' + - 'ee/spec/lib/gitlab/elastic/project_search_results_spec.rb' + - 'ee/spec/lib/gitlab/expiring_subscription_message_spec.rb' + - 'ee/spec/lib/gitlab/geo/log_cursor/daemon_spec.rb' + - 'ee/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb' + - 'ee/spec/lib/omni_auth/strategies/group_saml_spec.rb' + - 'ee/spec/lib/security/ci_configuration/sast_build_actions_spec.rb' + - 'ee/spec/lib/system_check/geo/geo_database_configured_check_spec.rb' + - 'ee/spec/migrations/schedule_populate_resolved_on_default_branch_column_spec.rb' + - 'ee/spec/migrations/update_location_fingerprint_column_for_cs_spec.rb' + - 'ee/spec/migrations/update_occurrence_severity_column_spec.rb' + - 'ee/spec/migrations/update_undefined_confidence_from_occurrences_spec.rb' + - 'ee/spec/migrations/update_undefined_confidence_from_vulnerabilities_spec.rb' + - 'ee/spec/migrations/update_vulnerability_severity_column_spec.rb' + - 'ee/spec/models/ee/namespace_spec.rb' + - 'ee/spec/models/geo_node_status_spec.rb' + - 'ee/spec/models/group_spec.rb' + - 'ee/spec/models/issue_spec.rb' + - 'ee/spec/models/merge_request_spec.rb' + - 'ee/spec/models/project_import_state_spec.rb' + - 'ee/spec/models/push_rule_spec.rb' + - 'ee/spec/presenters/ci/pipeline_presenter_spec.rb' + - 'ee/spec/presenters/projects/security/configuration_presenter_spec.rb' + - 'ee/spec/requests/api/geo_nodes_spec.rb' + - 'ee/spec/requests/api/graphql/mutations/dast_on_demand_scans/create_spec.rb' + - 'ee/spec/requests/api/graphql/mutations/dast_site_profiles/delete_spec.rb' + - 'ee/spec/requests/api/graphql/mutations/pipelines/run_dast_scan_spec.rb' + - 'ee/spec/requests/api/issues_spec.rb' + - 'ee/spec/requests/api/projects_spec.rb' + - 'ee/spec/requests/git_http_spec.rb' + - 'ee/spec/requests/groups_controller_spec.rb' + - 'ee/spec/requests/omniauth_kerberos_spnego_spec.rb' + - 'ee/spec/requests/repositories/git_http_controller_spec.rb' + - 'ee/spec/services/alert_management/network_alert_service_spec.rb' + - 'ee/spec/services/ci/expire_pipeline_cache_service_spec.rb' + - 'ee/spec/services/ci/run_dast_scan_service_spec.rb' + - 'ee/spec/services/ee/git/branch_push_service_spec.rb' + - 'ee/spec/services/ee/merge_requests/create_from_vulnerability_data_service_spec.rb' + - 'ee/spec/services/ee/merge_requests/refresh_service_spec.rb' + - 'ee/spec/services/ee/security/ingress_modsecurity_usage_service_spec.rb' + - 'ee/spec/services/ee/users/create_service_spec.rb' + - 'ee/spec/services/ee/users/destroy_service_spec.rb' + - 'ee/spec/services/geo/container_repository_sync_service_spec.rb' + - 'ee/spec/services/geo/design_repository_sync_service_spec.rb' + - 'ee/spec/services/geo/framework_repository_sync_service_spec.rb' + - 'ee/spec/services/geo/hashed_storage_migration_service_spec.rb' + - 'ee/spec/services/geo/metrics_update_service_spec.rb' + - 'ee/spec/services/geo/move_repository_service_spec.rb' + - 'ee/spec/services/geo/project_housekeeping_service_spec.rb' + - 'ee/spec/services/geo/rename_repository_service_spec.rb' + - 'ee/spec/services/geo/repository_destroy_service_spec.rb' + - 'ee/spec/services/geo/repository_sync_service_spec.rb' + - 'ee/spec/services/geo/wiki_sync_service_spec.rb' + - 'ee/spec/services/groups/destroy_service_spec.rb' + - 'ee/spec/services/groups/update_service_spec.rb' + - 'ee/spec/services/merge_trains/check_status_service_spec.rb' + - 'ee/spec/services/network_policies/resources_service_spec.rb' + - 'ee/spec/services/projects/destroy_service_spec.rb' + - 'ee/spec/services/projects/group_links/destroy_service_spec.rb' + - 'ee/spec/services/projects/update_service_spec.rb' + - 'ee/spec/services/slash_commands/global_slack_handler_spec.rb' + - 'ee/spec/support/helpers/ee/stub_configuration.rb' + - 'ee/spec/support/shared_examples/controllers/analytics/cycle_analytics/shared_stage_shared_examples.rb' + - 'ee/spec/support/shared_examples/features/gold_trial_callout_shared_examples.rb' + - 'ee/spec/support/shared_examples/lib/gitlab/geo/geo_logs_event_source_info_shared_examples.rb' + - 'ee/spec/support/shared_examples/models/member_shared_examples.rb' + - 'ee/spec/support/shared_examples/services/base_sync_service_shared_examples.rb' + - 'ee/spec/support/shared_examples/services/geo/geo_request_service_shared_examples.rb' + - 'ee/spec/workers/build_finished_worker_spec.rb' + - 'ee/spec/workers/concerns/elastic/indexing_control_spec.rb' + - 'ee/spec/workers/elastic_commit_indexer_worker_spec.rb' + - 'ee/spec/workers/geo/design_repository_shard_sync_worker_spec.rb' + - 'ee/spec/workers/geo/file_download_dispatch_worker_spec.rb' + - 'ee/spec/workers/geo/registry_sync_worker_spec.rb' + - 'ee/spec/workers/geo/repository_cleanup_worker_spec.rb' + - 'ee/spec/workers/geo/repository_shard_sync_worker_spec.rb' + - 'ee/spec/workers/project_cache_worker_spec.rb' + - 'ee/spec/workers/repository_import_worker_spec.rb' + - 'ee/spec/workers/vulnerability_exports/export_deletion_worker_spec.rb' + - 'qa/spec/runtime/release_spec.rb' + - 'spec/controllers/admin/sessions_controller_spec.rb' + - 'spec/controllers/application_controller_spec.rb' + - 'spec/controllers/concerns/issuable_actions_spec.rb' + - 'spec/controllers/concerns/static_object_external_storage_spec.rb' + - 'spec/controllers/explore/projects_controller_spec.rb' + - 'spec/controllers/groups/clusters_controller_spec.rb' + - 'spec/controllers/groups/settings/ci_cd_controller_spec.rb' + - 'spec/controllers/groups_controller_spec.rb' + - 'spec/controllers/import/bitbucket_controller_spec.rb' + - 'spec/controllers/oauth/jira/authorizations_controller_spec.rb' + - 'spec/controllers/omniauth_callbacks_controller_spec.rb' + - 'spec/controllers/projects/artifacts_controller_spec.rb' + - 'spec/controllers/projects/branches_controller_spec.rb' + - 'spec/controllers/projects/clusters_controller_spec.rb' + - 'spec/controllers/projects/commit_controller_spec.rb' + - 'spec/controllers/projects/commits_controller_spec.rb' + - 'spec/controllers/projects/environments_controller_spec.rb' + - 'spec/controllers/projects/imports_controller_spec.rb' + - 'spec/controllers/projects/issues_controller_spec.rb' + - 'spec/controllers/projects/jobs_controller_spec.rb' + - 'spec/controllers/projects/labels_controller_spec.rb' + - 'spec/controllers/projects/merge_requests_controller_spec.rb' + - 'spec/controllers/projects/pipelines_controller_spec.rb' + - 'spec/controllers/projects/service_hook_logs_controller_spec.rb' + - 'spec/controllers/projects/services_controller_spec.rb' + - 'spec/controllers/projects/tags_controller_spec.rb' + - 'spec/controllers/registrations/experience_levels_controller_spec.rb' + - 'spec/controllers/registrations_controller_spec.rb' + - 'spec/controllers/sessions_controller_spec.rb' + - 'spec/controllers/snippets/notes_controller_spec.rb' + - 'spec/controllers/snippets_controller_spec.rb' + - 'spec/features/admin/admin_mode/login_spec.rb' + - 'spec/features/groups/clusters/eks_spec.rb' + - 'spec/features/groups/members/tabs_spec.rb' + - 'spec/features/ide/static_object_external_storage_csp_spec.rb' + - 'spec/features/issuables/issuable_list_spec.rb' + - 'spec/features/issues/form_spec.rb' + - 'spec/features/merge_request/user_creates_image_diff_notes_spec.rb' + - 'spec/features/merge_request/user_reviews_image_spec.rb' + - 'spec/features/merge_request/user_sees_diff_spec.rb' + - 'spec/features/merge_request/user_sees_merge_widget_spec.rb' + - 'spec/features/profiles/personal_access_tokens_spec.rb' + - 'spec/features/projects/clusters/gcp_spec.rb' + - 'spec/features/projects/clusters_spec.rb' + - 'spec/features/projects/container_registry_spec.rb' + - 'spec/features/projects/files/user_browses_lfs_files_spec.rb' + - 'spec/features/projects/jobs_spec.rb' + - 'spec/features/projects/navbar_spec.rb' + - 'spec/features/projects/pages_spec.rb' + - 'spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb' + - 'spec/features/projects/settings/service_desk_setting_spec.rb' + - 'spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb' + - 'spec/features/snippets/embedded_snippet_spec.rb' + - 'spec/features/usage_stats_consent_spec.rb' + - 'spec/finders/prometheus_metrics_finder_spec.rb' + - 'spec/graphql/mutations/alert_management/create_alert_issue_spec.rb' + - 'spec/graphql/mutations/alert_management/http_integration/create_spec.rb' + - 'spec/graphql/mutations/alert_management/http_integration/destroy_spec.rb' + - 'spec/graphql/mutations/alert_management/http_integration/reset_token_spec.rb' + - 'spec/graphql/mutations/alert_management/http_integration/update_spec.rb' + - 'spec/graphql/mutations/alert_management/prometheus_integration/create_spec.rb' + - 'spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb' + - 'spec/graphql/mutations/alert_management/prometheus_integration/update_spec.rb' + - 'spec/helpers/analytics/unique_visits_helper_spec.rb' + - 'spec/helpers/projects_helper_spec.rb' + - 'spec/initializers/lograge_spec.rb' + - 'spec/lib/api/entities/merge_request_basic_spec.rb' + - 'spec/lib/api/entities/merge_request_changes_spec.rb' + - 'spec/lib/api/helpers_spec.rb' + - 'spec/lib/backup/files_spec.rb' + - 'spec/lib/backup/manager_spec.rb' + - 'spec/lib/banzai/commit_renderer_spec.rb' + - 'spec/lib/banzai/filter/external_issue_reference_filter_spec.rb' + - 'spec/lib/banzai/filter/issue_reference_filter_spec.rb' + - 'spec/lib/banzai/filter/repository_link_filter_spec.rb' + - 'spec/lib/banzai/pipeline/gfm_pipeline_spec.rb' + - 'spec/lib/extracts_ref_spec.rb' + - 'spec/lib/feature_spec.rb' + - 'spec/lib/gitlab/app_logger_spec.rb' + - 'spec/lib/gitlab/asciidoc_spec.rb' + - 'spec/lib/gitlab/auth/auth_finders_spec.rb' + - 'spec/lib/gitlab/auth/blocked_user_tracker_spec.rb' + - 'spec/lib/gitlab/auth/request_authenticator_spec.rb' + - 'spec/lib/gitlab/auth_spec.rb' + - 'spec/lib/gitlab/background_migration/populate_personal_snippet_statistics_spec.rb' + - 'spec/lib/gitlab/background_migration/populate_project_snippet_statistics_spec.rb' + - 'spec/lib/gitlab/checks/diff_check_spec.rb' + - 'spec/lib/gitlab/checks/lfs_check_spec.rb' + - 'spec/lib/gitlab/checks/lfs_integrity_spec.rb' + - 'spec/lib/gitlab/ci/config/external/file/base_spec.rb' + - 'spec/lib/gitlab/ci/config/external/file/local_spec.rb' + - 'spec/lib/gitlab/ci/config/external/processor_spec.rb' + - 'spec/lib/gitlab/ci/pipeline/chain/build_spec.rb' + - 'spec/lib/gitlab/ci/pipeline/chain/command_spec.rb' + - 'spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb' + - 'spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb' + - 'spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb' + - 'spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb' + - 'spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb' + - 'spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb' + - 'spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb' + - 'spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb' + - 'spec/lib/gitlab/ci/templates/npm_spec.rb' + - 'spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb' + - 'spec/lib/gitlab/ci/trace_spec.rb' + - 'spec/lib/gitlab/current_settings_spec.rb' + - 'spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb' + - 'spec/lib/gitlab/database/multi_threaded_migration_spec.rb' + - 'spec/lib/gitlab/diff/highlight_cache_spec.rb' + - 'spec/lib/gitlab/diff/highlight_spec.rb' + - 'spec/lib/gitlab/diff/position_spec.rb' + - 'spec/lib/gitlab/email/handler/create_issue_handler_spec.rb' + - 'spec/lib/gitlab/email/handler/create_note_handler_spec.rb' + - 'spec/lib/gitlab/etag_caching/middleware_spec.rb' + - 'spec/lib/gitlab/exclusive_lease_helpers_spec.rb' + - 'spec/lib/gitlab/fogbugz_import/importer_spec.rb' + - 'spec/lib/gitlab/gfm/reference_rewriter_spec.rb' + - 'spec/lib/gitlab/git/repository_spec.rb' + - 'spec/lib/gitlab/gitaly_client/blob_service_spec.rb' + - 'spec/lib/gitlab/gitaly_client/commit_service_spec.rb' + - 'spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb' + - 'spec/lib/gitlab/gitaly_client/health_check_service_spec.rb' + - 'spec/lib/gitlab/gitaly_client/operation_service_spec.rb' + - 'spec/lib/gitlab/gitaly_client/praefect_info_service_spec.rb' + - 'spec/lib/gitlab/gitaly_client/ref_service_spec.rb' + - 'spec/lib/gitlab/gitaly_client/remote_service_spec.rb' + - 'spec/lib/gitlab/gitaly_client/repository_service_spec.rb' + - 'spec/lib/gitlab/gitaly_client/wiki_service_spec.rb' + - 'spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb' + - 'spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb' + - 'spec/lib/gitlab/hashed_storage/migrator_spec.rb' + - 'spec/lib/gitlab/import/merge_request_helpers_spec.rb' + - 'spec/lib/gitlab/import_export/config_spec.rb' + - 'spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb' + - 'spec/lib/gitlab/import_export/importer_spec.rb' + - 'spec/lib/gitlab/import_export/lfs_restorer_spec.rb' + - 'spec/lib/gitlab/import_export/project/tree_restorer_spec.rb' + - 'spec/lib/gitlab/import_export/snippet_repo_restorer_spec.rb' + - 'spec/lib/gitlab/import_export/snippets_repo_restorer_spec.rb' + - 'spec/lib/gitlab/import_export/version_checker_spec.rb' + - 'spec/lib/gitlab/job_waiter_spec.rb' + - 'spec/lib/gitlab/legacy_github_import/importer_spec.rb' + - 'spec/lib/gitlab/legacy_github_import/project_creator_spec.rb' + - 'spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb' + - 'spec/lib/gitlab/metrics/rack_middleware_spec.rb' + - 'spec/lib/gitlab/metrics/subscribers/active_record_spec.rb' + - 'spec/lib/gitlab/metrics_spec.rb' + - 'spec/lib/gitlab/patch/action_dispatch_journey_formatter_spec.rb' + - 'spec/lib/gitlab/sidekiq_daemon/monitor_spec.rb' + - 'spec/lib/gitlab/sidekiq_middleware_spec.rb' + - 'spec/lib/gitlab/tracking/destinations/product_analytics_spec.rb' + - 'spec/lib/gitlab/tracking/destinations/snowplow_spec.rb' + - 'spec/lib/gitlab/tracking_spec.rb' + - 'spec/lib/gitlab/usage_data_spec.rb' + - 'spec/lib/gitlab/workhorse_spec.rb' + - 'spec/lib/gitlab/x509/commit_spec.rb' + - 'spec/lib/gitlab/x509/signature_spec.rb' + - 'spec/lib/google_api/cloud_platform/client_spec.rb' + - 'spec/lib/json_web_token/rsa_token_spec.rb' + - 'spec/lib/mattermost/command_spec.rb' + - 'spec/lib/mattermost/team_spec.rb' + - 'spec/lib/system_check/simple_executor_spec.rb' + - 'spec/models/ci/build_spec.rb' + - 'spec/models/ci/runner_spec.rb' + - 'spec/models/commit_spec.rb' + - 'spec/models/environment_spec.rb' + - 'spec/models/group_spec.rb' + - 'spec/models/hooks/service_hook_spec.rb' + - 'spec/models/hooks/system_hook_spec.rb' + - 'spec/models/hooks/web_hook_spec.rb' + - 'spec/models/issue_spec.rb' + - 'spec/models/key_spec.rb' + - 'spec/models/member_spec.rb' + - 'spec/models/merge_request_diff_spec.rb' + - 'spec/models/merge_request_spec.rb' + - 'spec/models/note_spec.rb' + - 'spec/models/project_import_state_spec.rb' + - 'spec/models/project_services/jira_service_spec.rb' + - 'spec/models/project_services/mattermost_slash_commands_service_spec.rb' + - 'spec/models/project_spec.rb' + - 'spec/models/repository_spec.rb' + - 'spec/models/user_spec.rb' + - 'spec/models/x509_certificate_spec.rb' + - 'spec/policies/ci/build_policy_spec.rb' + - 'spec/policies/ci/pipeline_policy_spec.rb' + - 'spec/presenters/gitlab/blame_presenter_spec.rb' + - 'spec/presenters/merge_request_presenter_spec.rb' + - 'spec/requests/api/api_spec.rb' + - 'spec/requests/api/ci/runner/jobs_artifacts_spec.rb' + - 'spec/requests/api/ci/runner/jobs_put_spec.rb' + - 'spec/requests/api/ci/runner/jobs_request_post_spec.rb' + - 'spec/requests/api/ci/runner/jobs_trace_spec.rb' + - 'spec/requests/api/ci/runner/runners_delete_spec.rb' + - 'spec/requests/api/ci/runner/runners_post_spec.rb' + - 'spec/requests/api/ci/runner/runners_verify_post_spec.rb' + - 'spec/requests/api/graphql/gitlab_schema_spec.rb' + - 'spec/requests/api/graphql/project/error_tracking/sentry_detailed_error_request_spec.rb' + - 'spec/requests/api/graphql/project/error_tracking/sentry_errors_request_spec.rb' + - 'spec/requests/api/graphql_spec.rb' + - 'spec/requests/api/helpers_spec.rb' + - 'spec/requests/api/internal/base_spec.rb' + - 'spec/requests/api/maven_packages_spec.rb' + - 'spec/requests/api/merge_requests_spec.rb' + - 'spec/requests/api/pages/pages_spec.rb' + - 'spec/requests/api/project_export_spec.rb' + - 'spec/requests/api/project_import_spec.rb' + - 'spec/requests/api/projects_spec.rb' + - 'spec/requests/api/snippets_spec.rb' + - 'spec/requests/api/todos_spec.rb' + - 'spec/requests/git_http_spec.rb' + - 'spec/requests/import/gitlab_projects_controller_spec.rb' + - 'spec/routing/routing_spec.rb' + - 'spec/serializers/analytics_stage_serializer_spec.rb' + - 'spec/serializers/merge_request_poll_cached_widget_entity_spec.rb' + - 'spec/serializers/merge_request_poll_widget_entity_spec.rb' + - 'spec/services/application_settings/update_service_spec.rb' + - 'spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb' + - 'spec/services/boards/lists/update_service_spec.rb' + - 'spec/services/ci/create_pipeline_service_spec.rb' + - 'spec/services/ci/destroy_expired_job_artifacts_service_spec.rb' + - 'spec/services/ci/expire_pipeline_cache_service_spec.rb' + - 'spec/services/ci/list_config_variables_service_spec.rb' + - 'spec/services/ci/register_job_service_spec.rb' + - 'spec/services/ci/resource_groups/assign_resource_from_resource_group_service_spec.rb' + - 'spec/services/ci/retry_build_service_spec.rb' + - 'spec/services/ci/retry_pipeline_service_spec.rb' + - 'spec/services/ci/stop_environments_service_spec.rb' + - 'spec/services/clusters/applications/create_service_spec.rb' + - 'spec/services/clusters/cleanup/project_namespace_service_spec.rb' + - 'spec/services/clusters/cleanup/service_account_service_spec.rb' + - 'spec/services/deployments/older_deployments_drop_service_spec.rb' + - 'spec/services/deployments/update_environment_service_spec.rb' + - 'spec/services/draft_notes/destroy_service_spec.rb' + - 'spec/services/events/render_service_spec.rb' + - 'spec/services/git/branch_push_service_spec.rb' + - 'spec/services/git/process_ref_changes_service_spec.rb' + - 'spec/services/groups/create_service_spec.rb' + - 'spec/services/groups/update_service_spec.rb' + - 'spec/services/integrations/test/project_service_spec.rb' + - 'spec/services/issuable/destroy_service_spec.rb' + - 'spec/services/issues/close_service_spec.rb' + - 'spec/services/issues/reopen_service_spec.rb' + - 'spec/services/members/destroy_service_spec.rb' + - 'spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb' + - 'spec/services/merge_requests/build_service_spec.rb' + - 'spec/services/merge_requests/merge_service_spec.rb' + - 'spec/services/merge_requests/mergeability_check_service_spec.rb' + - 'spec/services/merge_requests/refresh_service_spec.rb' + - 'spec/services/merge_requests/reload_diffs_service_spec.rb' + - 'spec/services/merge_requests/resolved_discussion_notification_service_spec.rb' + - 'spec/services/metrics/dashboard/custom_dashboard_service_spec.rb' + - 'spec/services/metrics/dashboard/transient_embed_service_spec.rb' + - 'spec/services/notes/create_service_spec.rb' + - 'spec/services/notes/render_service_spec.rb' + - 'spec/services/packages/conan/create_package_file_service_spec.rb' + - 'spec/services/packages/nuget/metadata_extraction_service_spec.rb' + - 'spec/services/packages/nuget/update_package_from_metadata_service_spec.rb' + - 'spec/services/pages/delete_services_spec.rb' + - 'spec/services/pod_logs/elasticsearch_service_spec.rb' + - 'spec/services/pod_logs/kubernetes_service_spec.rb' + - 'spec/services/post_receive_service_spec.rb' + - 'spec/services/projects/after_rename_service_spec.rb' + - 'spec/services/projects/container_repository/cleanup_tags_service_spec.rb' + - 'spec/services/projects/container_repository/delete_tags_service_spec.rb' + - 'spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb' + - 'spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb' + - 'spec/services/projects/destroy_service_spec.rb' + - 'spec/services/projects/fork_service_spec.rb' + - 'spec/services/projects/import_service_spec.rb' + - 'spec/services/projects/lfs_pointers/lfs_download_service_spec.rb' + - 'spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb' + - 'spec/services/projects/prometheus/alerts/notify_service_spec.rb' + - 'spec/services/projects/transfer_service_spec.rb' + - 'spec/services/projects/update_remote_mirror_service_spec.rb' + - 'spec/services/projects/update_service_spec.rb' + - 'spec/services/projects/update_statistics_service_spec.rb' + - 'spec/services/resource_events/change_labels_service_spec.rb' + - 'spec/services/search_service_spec.rb' + - 'spec/services/snippets/create_service_spec.rb' + - 'spec/services/test_hooks/project_service_spec.rb' + - 'spec/services/test_hooks/system_service_spec.rb' + - 'spec/services/todo_service_spec.rb' + - 'spec/services/users/destroy_service_spec.rb' + - 'spec/services/users/migrate_to_ghost_user_service_spec.rb' + - 'spec/spec_helper.rb' + - 'spec/support/capybara.rb' + - 'spec/support/helpers/api_helpers.rb' + - 'spec/support/helpers/graphql_helpers.rb' + - 'spec/support/helpers/ldap_helpers.rb' + - 'spec/support/helpers/login_helpers.rb' + - 'spec/support/helpers/metrics_dashboard_url_helpers.rb' + - 'spec/support/helpers/rake_helpers.rb' + - 'spec/support/helpers/stub_configuration.rb' + - 'spec/support/helpers/stub_gitlab_calls.rb' + - 'spec/support/helpers/test_env.rb' + - 'spec/support/import_export/common_util.rb' + - 'spec/support/services/migrate_to_ghost_user_service_shared_examples.rb' + - 'spec/support/shared_contexts/email_shared_context.rb' + - 'spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb' + - 'spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb' + - 'spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb' + - 'spec/support/shared_examples/controllers/issuables_requiring_filter_shared_examples.rb' + - 'spec/support/shared_examples/controllers/repository_lfs_file_load_shared_examples.rb' + - 'spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb' + - 'spec/support/shared_examples/controllers/unique_visits_shared_examples.rb' + - 'spec/support/shared_examples/controllers/update_invalid_issuable_shared_examples.rb' + - 'spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb' + - 'spec/support/shared_examples/features/archive_download_buttons_shared_examples.rb' + - 'spec/support/shared_examples/features/snippets_shared_examples.rb' + - 'spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb' + - 'spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb' + - 'spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb' + - 'spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb' + - 'spec/support/shared_examples/models/mentionable_shared_examples.rb' + - 'spec/support/shared_examples/models/with_uploads_shared_examples.rb' + - 'spec/support/shared_examples/path_extraction_shared_examples.rb' + - 'spec/support/shared_examples/requests/api/discussions_shared_examples.rb' + - 'spec/support/shared_examples/requests/api/snippets_shared_examples.rb' + - 'spec/support/shared_examples/requests/rack_attack_shared_examples.rb' + - 'spec/support/shared_examples/requests/snippet_shared_examples.rb' + - 'spec/support/shared_examples/services/alert_management_shared_examples.rb' + - 'spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb' + - 'spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb' + - 'spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb' + - 'spec/support/shared_examples/services/issuable_shared_examples.rb' + - 'spec/support/shared_examples/uploaders/object_storage_shared_examples.rb' + - 'spec/support/shared_examples/workers/authorized_projects_worker_shared_example.rb' + - 'spec/support/shared_examples/workers/reactive_cacheable_shared_examples.rb' + - 'spec/support/snowplow.rb' + - 'spec/support/unicorn.rb' + - 'spec/tasks/gitlab/cleanup_rake_spec.rb' + - 'spec/tasks/gitlab/container_registry_rake_spec.rb' + - 'spec/tasks/gitlab/db_rake_spec.rb' + - 'spec/tasks/gitlab/git_rake_spec.rb' + - 'spec/tasks/gitlab/praefect_rake_spec.rb' + - 'spec/tasks/gitlab/shell_rake_spec.rb' + - 'spec/tasks/gitlab/x509/update_rake_spec.rb' + - 'spec/uploaders/file_mover_spec.rb' + - 'spec/uploaders/records_uploads_spec.rb' + - 'spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb' + - 'spec/views/layouts/_head.html.haml_spec.rb' + - 'spec/views/projects/artifacts/_artifact.html.haml_spec.rb' + - 'spec/views/shared/runners/show.html.haml_spec.rb' + - 'spec/workers/archive_trace_worker_spec.rb' + - 'spec/workers/build_coverage_worker_spec.rb' + - 'spec/workers/build_hooks_worker_spec.rb' + - 'spec/workers/build_trace_sections_worker_spec.rb' + - 'spec/workers/ci/build_schedule_worker_spec.rb' + - 'spec/workers/ci/daily_build_group_report_results_worker_spec.rb' + - 'spec/workers/cluster_configure_istio_worker_spec.rb' + - 'spec/workers/cluster_provision_worker_spec.rb' + - 'spec/workers/clusters/cleanup/project_namespace_worker_spec.rb' + - 'spec/workers/clusters/cleanup/service_account_worker_spec.rb' + - 'spec/workers/concerns/project_import_options_spec.rb' + - 'spec/workers/create_commit_signature_worker_spec.rb' + - 'spec/workers/create_note_diff_file_worker_spec.rb' + - 'spec/workers/delete_diff_files_worker_spec.rb' + - 'spec/workers/email_receiver_worker_spec.rb' + - 'spec/workers/emails_on_push_worker_spec.rb' + - 'spec/workers/error_tracking_issue_link_worker_spec.rb' + - 'spec/workers/expire_pipeline_cache_worker_spec.rb' + - 'spec/workers/git_garbage_collect_worker_spec.rb' + - 'spec/workers/group_export_worker_spec.rb' + - 'spec/workers/group_import_worker_spec.rb' + - 'spec/workers/namespaceless_project_destroy_worker_spec.rb' + - 'spec/workers/namespaces/root_statistics_worker_spec.rb' + - 'spec/workers/new_note_worker_spec.rb' + - 'spec/workers/object_pool/create_worker_spec.rb' + - 'spec/workers/packages/nuget/extraction_worker_spec.rb' + - 'spec/workers/pages_remove_worker_spec.rb' + - 'spec/workers/pipeline_hooks_worker_spec.rb' + - 'spec/workers/pipeline_process_worker_spec.rb' + - 'spec/workers/pipeline_schedule_worker_spec.rb' + - 'spec/workers/project_cache_worker_spec.rb' + - 'spec/workers/stage_update_worker_spec.rb' + - 'spec/workers/stuck_ci_jobs_worker_spec.rb' + - 'spec/workers/wait_for_cluster_creation_worker_spec.rb' + - 'ee/spec/workers/security/auto_fix_worker_spec.rb' diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue new file mode 100644 index 00000000000..153c58b556e --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue @@ -0,0 +1,78 @@ +<script> +import { GlLoadingIcon } from '@gitlab/ui'; +import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql'; +import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql'; +import setTargetNamespaceMutation from '../graphql/mutations/set_target_namespace.mutation.graphql'; +import setNewNameMutation from '../graphql/mutations/set_new_name.mutation.graphql'; +import importGroupMutation from '../graphql/mutations/import_group.mutation.graphql'; +import ImportTableRow from './import_table_row.vue'; + +const mapApolloMutations = mutations => + Object.fromEntries( + Object.entries(mutations).map(([key, mutation]) => [ + key, + function mutate(config) { + return this.$apollo.mutate({ + mutation, + ...config, + }); + }, + ]), + ); + +export default { + components: { + GlLoadingIcon, + ImportTableRow, + }, + + apollo: { + bulkImportSourceGroups: bulkImportSourceGroupsQuery, + availableNamespaces: availableNamespacesQuery, + }, + + methods: { + ...mapApolloMutations({ + setTargetNamespace: setTargetNamespaceMutation, + setNewName: setNewNameMutation, + importGroup: importGroupMutation, + }), + }, +}; +</script> + +<template> + <div> + <gl-loading-icon v-if="$apollo.loading" size="md" class="gl-mt-5" /> + <div v-else-if="bulkImportSourceGroups.length"> + <table class="gl-w-full"> + <thead class="gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1"> + <th class="gl-py-4 import-jobs-from-col">{{ s__('BulkImport|From source group') }}</th> + <th class="gl-py-4 import-jobs-to-col">{{ s__('BulkImport|To new group') }}</th> + <th class="gl-py-4 import-jobs-status-col">{{ __('Status') }}</th> + <th class="gl-py-4 import-jobs-cta-col"></th> + </thead> + <tbody> + <template v-for="group in bulkImportSourceGroups"> + <import-table-row + :key="group.id" + :group="group" + :available-namespaces="availableNamespaces" + @update-target-namespace=" + setTargetNamespace({ + variables: { sourceGroupId: group.id, targetNamespace: $event }, + }) + " + @update-new-name=" + setNewName({ + variables: { sourceGroupId: group.id, newName: $event }, + }) + " + @import-group="importGroup({ variables: { sourceGroupId: group.id } })" + /> + </template> + </tbody> + </table> + </div> + </div> +</template> diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue new file mode 100644 index 00000000000..07603d89f0f --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue @@ -0,0 +1,106 @@ +<script> +import { GlButton, GlIcon, GlLink, GlFormInput } from '@gitlab/ui'; +import { joinPaths } from '~/lib/utils/url_utility'; +import Select2Select from '~/vue_shared/components/select2_select.vue'; +import ImportStatus from '../../components/import_status.vue'; +import { STATUSES } from '../../constants'; + +export default { + components: { + Select2Select, + ImportStatus, + GlButton, + GlLink, + GlIcon, + GlFormInput, + }, + props: { + group: { + type: Object, + required: true, + }, + availableNamespaces: { + type: Array, + required: true, + }, + }, + computed: { + isDisabled() { + return this.group.status !== STATUSES.NONE; + }, + + isFinished() { + return this.group.status === STATUSES.FINISHED; + }, + + select2Options() { + return { + data: this.availableNamespaces.map(namespace => ({ + id: namespace.full_path, + text: namespace.full_path, + })), + }; + }, + }, + methods: { + getPath(group) { + return `${group.import_target.target_namespace}/${group.import_target.new_name}`; + }, + + getFullPath(group) { + return joinPaths(gon.relative_url_root || '/', this.getPath(group)); + }, + }, +}; +</script> + +<template> + <tr class="gl-border-gray-200 gl-border-0 gl-border-b-1"> + <td class="gl-p-4"> + <gl-link :href="group.web_url" target="_blank"> + {{ group.full_path }} <gl-icon name="external-link" /> + </gl-link> + </td> + <td class="gl-p-4"> + <gl-link v-if="isFinished" :href="getFullPath(group)">{{ getPath(group) }}</gl-link> + + <div + v-else + class="import-entities-target-select gl-display-flex gl-align-items-stretch" + :class="{ + disabled: isDisabled, + }" + > + <select2-select + :disabled="isDisabled" + :options="select2Options" + :value="group.import_target.target_namespace" + @input="$emit('update-target-namespace', $event)" + /> + <div + class="import-entities-target-select-separator gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1" + > + / + </div> + <gl-form-input + class="gl-rounded-top-left-none gl-rounded-bottom-left-none" + :disabled="isDisabled" + :value="group.import_target.new_name" + @input="$emit('update-new-name', $event)" + /> + </div> + </td> + <td class="gl-p-4 gl-white-space-nowrap"> + <import-status :status="group.status" /> + </td> + <td class="gl-p-4"> + <gl-button + v-if="!isDisabled" + variant="success" + category="secondary" + @click="$emit('import-group')" + >{{ __('Import') }}</gl-button + > + </td> + </tr> +</template> diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js new file mode 100644 index 00000000000..23f4190c2d0 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js @@ -0,0 +1,66 @@ +import axios from '~/lib/utils/axios_utils'; +import createDefaultClient from '~/lib/graphql'; +import { STATUSES } from '../../constants'; +import availableNamespacesQuery from './queries/available_namespaces.query.graphql'; +import { SourceGroupsManager } from './services/source_groups_manager'; + +export const clientTypenames = { + BulkImportSourceGroup: 'ClientBulkImportSourceGroup', + AvailableNamespace: 'ClientAvailableNamespace', +}; + +export function createResolvers({ endpoints }) { + return { + Query: { + async bulkImportSourceGroups(_, __, { client }) { + const { + data: { availableNamespaces }, + } = await client.query({ query: availableNamespacesQuery }); + + return axios.get(endpoints.status).then(({ data }) => { + return data.importable_data.map(group => ({ + __typename: clientTypenames.BulkImportSourceGroup, + ...group, + status: STATUSES.NONE, + import_target: { + new_name: group.full_path, + target_namespace: availableNamespaces[0].full_path, + }, + })); + }); + }, + + availableNamespaces: () => + axios.get(endpoints.availableNamespaces).then(({ data }) => + data.map(namespace => ({ + __typename: clientTypenames.AvailableNamespace, + ...namespace, + })), + ), + }, + Mutation: { + setTargetNamespace(_, { targetNamespace, sourceGroupId }, { client }) { + new SourceGroupsManager({ client }).updateById(sourceGroupId, sourceGroup => { + // eslint-disable-next-line no-param-reassign + sourceGroup.import_target.target_namespace = targetNamespace; + }); + }, + + setNewName(_, { newName, sourceGroupId }, { client }) { + new SourceGroupsManager({ client }).updateById(sourceGroupId, sourceGroup => { + // eslint-disable-next-line no-param-reassign + sourceGroup.import_target.new_name = newName; + }); + }, + + async importGroup(_, { sourceGroupId }, { client }) { + const groupManager = new SourceGroupsManager({ client }); + const group = groupManager.findById(sourceGroupId); + groupManager.setImportStatus(group, STATUSES.SCHEDULING); + }, + }, + }; +} + +export const createApolloClient = ({ endpoints }) => + createDefaultClient(createResolvers({ endpoints }), { assumeImmutableResults: true }); diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql new file mode 100644 index 00000000000..50774e36599 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql @@ -0,0 +1,8 @@ +fragment BulkImportSourceGroupItem on ClientBulkImportSourceGroup { + id + web_url + full_path + full_name + status + import_target +} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql new file mode 100644 index 00000000000..412608d3faf --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql @@ -0,0 +1,3 @@ +mutation importGroup($sourceGroupId: String!) { + importGroup(sourceGroupId: $sourceGroupId) @client +} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql new file mode 100644 index 00000000000..2bc19891401 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql @@ -0,0 +1,3 @@ +mutation setNewName($newName: String!, $sourceGroupId: String!) { + setNewName(newName: $newName, sourceGroupId: $sourceGroupId) @client +} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql new file mode 100644 index 00000000000..fc98a1652c1 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql @@ -0,0 +1,3 @@ +mutation setTargetNamespace($targetNamespace: String!, $sourceGroupId: String!) { + setTargetNamespace(targetNamespace: $targetNamespace, sourceGroupId: $sourceGroupId) @client +} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql new file mode 100644 index 00000000000..5ab9796b50a --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql @@ -0,0 +1,6 @@ +query availableNamespaces { + availableNamespaces @client { + id + full_path + } +} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql new file mode 100644 index 00000000000..8d52d94925c --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql @@ -0,0 +1,7 @@ +#import "../fragments/bulk_import_source_group_item.fragment.graphql" + +query bulkImportSourceGroups { + bulkImportSourceGroups @client { + ...BulkImportSourceGroupItem + } +} diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js new file mode 100644 index 00000000000..f752ecc8cd6 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js @@ -0,0 +1,45 @@ +import { defaultDataIdFromObject } from 'apollo-cache-inmemory'; +import produce from 'immer'; +import ImportSourceGroupFragment from '../fragments/bulk_import_source_group_item.fragment.graphql'; + +function extractTypeConditionFromFragment(fragment) { + return fragment.definitions[0]?.typeCondition.name.value; +} + +function generateGroupId(id) { + return defaultDataIdFromObject({ + __typename: extractTypeConditionFromFragment(ImportSourceGroupFragment), + id, + }); +} + +export class SourceGroupsManager { + constructor({ client }) { + this.client = client; + } + + findById(id) { + const cacheId = generateGroupId(id); + return this.client.readFragment({ fragment: ImportSourceGroupFragment, id: cacheId }); + } + + update(group, fn) { + this.client.writeFragment({ + fragment: ImportSourceGroupFragment, + id: generateGroupId(group.id), + data: produce(group, fn), + }); + } + + updateById(id, fn) { + const group = this.findById(id); + this.update(group, fn); + } + + setImportStatus(group, status) { + this.update(group, sourceGroup => { + // eslint-disable-next-line no-param-reassign + sourceGroup.status = status; + }); + } +} diff --git a/app/assets/javascripts/import_entities/import_groups/index.js b/app/assets/javascripts/import_entities/import_groups/index.js new file mode 100644 index 00000000000..bf427075564 --- /dev/null +++ b/app/assets/javascripts/import_entities/import_groups/index.js @@ -0,0 +1,31 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import Translate from '~/vue_shared/translate'; +import { createApolloClient } from './graphql/client_factory'; +import ImportTable from './components/import_table.vue'; + +Vue.use(Translate); +Vue.use(VueApollo); + +export function mountImportGroupsApp(mountElement) { + if (!mountElement) return undefined; + + const { statusPath, availableNamespacesPath, createBulkImportPath } = mountElement.dataset; + const apolloProvider = new VueApollo({ + defaultClient: createApolloClient({ + endpoints: { + status: statusPath, + availableNamespaces: availableNamespacesPath, + createBulkImport: createBulkImportPath, + }, + }), + }); + + return new Vue({ + el: mountElement, + apolloProvider, + render(createElement) { + return createElement(ImportTable); + }, + }); +} diff --git a/app/assets/javascripts/pages/import/bulk_imports/index.js b/app/assets/javascripts/pages/import/bulk_imports/index.js new file mode 100644 index 00000000000..37ac1a98466 --- /dev/null +++ b/app/assets/javascripts/pages/import/bulk_imports/index.js @@ -0,0 +1,4 @@ +import { mountImportGroupsApp } from '~/import_entities/import_groups'; + +const mountElement = document.getElementById('import-groups-mount-element'); +mountImportGroupsApp(mountElement); diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue index 8b5bed08931..4bf837faed1 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue @@ -1,7 +1,7 @@ <script> import dateFormat from 'dateformat'; import { GlColumnChart } from '@gitlab/ui/dist/charts'; -import { GlAlert } from '@gitlab/ui'; +import { GlAlert, GlSkeletonLoader } from '@gitlab/ui'; import { __, s__, sprintf } from '~/locale'; import { getDateInPast } from '~/lib/utils/datetime_utility'; import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql'; @@ -50,6 +50,7 @@ export default { components: { GlAlert, GlColumnChart, + GlSkeletonLoader, StatisticsList, PipelinesAreaChart, }, @@ -278,7 +279,8 @@ export default { <h4 class="gl-my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4> <div class="row"> <div class="col-md-6"> - <statistics-list :counts="formattedCounts" /> + <gl-skeleton-loader v-if="$apollo.queries.counts.loading" :lines="5" /> + <statistics-list v-else :counts="formattedCounts" /> </div> <div class="col-md-6"> <strong> diff --git a/app/assets/stylesheets/page_bundles/import.scss b/app/assets/stylesheets/page_bundles/import.scss index 2a154393630..5f43d5df7e3 100644 --- a/app/assets/stylesheets/page_bundles/import.scss +++ b/app/assets/stylesheets/page_bundles/import.scss @@ -43,9 +43,33 @@ } .import-entities-target-select { + &.disabled { + .import-entities-target-select-separator, + .select2-container.select2-container-disabled .select2-choice { + color: var(--gray-400, $gray-400); + border-color: var(--gray-100, $gray-100); + background-color: var(--gray-10, $gray-10); + } + + .select2-container.select2-container-disabled .select2-choice .select2-arrow { + background-color: var(--gray-10, $gray-10); + } + } + + .import-entities-target-select-separator { + border-color: var(--gray-200, $gray-200); + background-color: var(--gray-10, $gray-10); + } + .select2-container { > .select2-choice { + .select2-arrow { + background-color: var(--white, $white); + } + border-color: var(--gray-200, $gray-200); + color: var(--gray-900, $gray-900) !important; + background-color: var(--white, $white) !important; border-top-right-radius: 0; border-bottom-right-radius: 0; } diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb index 65ab835c33c..1ae90edd8f7 100644 --- a/app/controllers/concerns/wiki_actions.rb +++ b/app/controllers/concerns/wiki_actions.rb @@ -186,6 +186,10 @@ module WikiActions end # rubocop:enable Gitlab/ModuleWithInstanceVariables + def git_access + render 'shared/wikis/git_access' + end + private def container diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 8f794512486..d1486f765e4 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -6,7 +6,4 @@ class Projects::WikisController < Projects::ApplicationController alias_method :container, :project feature_category :wiki - - def git_access - end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index f50b3297e7e..7866e3e3d9f 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -255,6 +255,7 @@ module ApplicationSettingsHelper :password_authentication_enabled_for_git, :performance_bar_allowed_group_path, :performance_bar_enabled, + :personal_access_token_prefix, :kroki_enabled, :kroki_url, :plantuml_enabled, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 007de5812ee..9b9db7f93fd 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -249,6 +249,12 @@ class ApplicationSetting < ApplicationRecord validates :user_default_internal_regex, js_regex: true, allow_nil: true + validates :personal_access_token_prefix, + format: { with: /\A[a-zA-Z0-9_+=\/@:.-]+\z/, + message: _("can contain only letters of the Base64 alphabet (RFC4648) with the addition of '@', ':' and '.'") }, + length: { maximum: 20, message: _('is too long (maximum is %{count} characters)') }, + allow_blank: true + validates :commit_email_hostname, format: { with: /\A[^@]+\z/ } validates :archive_builds_in_seconds, diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 9c6f25d1986..105889a364a 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -104,6 +104,7 @@ module ApplicationSettingImplementation password_authentication_enabled_for_git: true, password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'], performance_bar_allowed_group_id: nil, + personal_access_token_prefix: nil, plantuml_enabled: false, plantuml_url: nil, polling_interval_multiplier: 1, diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index fbc02ed9cb0..462f09bf1b9 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -104,7 +104,6 @@ module Ci accepts_nested_attributes_for :variables, reject_if: :persisted? - delegate :id, to: :project, prefix: true delegate :full_path, to: :project, prefix: true validates :sha, presence: { unless: :importing? } diff --git a/app/models/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb index abddbf1c7e3..31b5afd604d 100644 --- a/app/models/concerns/case_sensitivity.rb +++ b/app/models/concerns/case_sensitivity.rb @@ -11,12 +11,14 @@ module CaseSensitivity def iwhere(params) criteria = self - params.each do |key, value| + params.each do |column, value| + column = arel_table[column] unless column.is_a?(Arel::Attribute) + criteria = case value when Array - criteria.where(value_in(key, value)) + criteria.where(value_in(column, value)) else - criteria.where(value_equal(key, value)) + criteria.where(value_equal(column, value)) end end @@ -28,7 +30,7 @@ module CaseSensitivity def value_equal(column, value) lower_value = lower_value(value) - lower_column(arel_table[column]).eq(lower_value).to_sql + lower_column(column).eq(lower_value).to_sql end def value_in(column, values) @@ -36,7 +38,7 @@ module CaseSensitivity lower_value(value) end - lower_column(arel_table[column]).in(lower_values).to_sql + lower_column(column).in(lower_values).to_sql end def lower_value(value) diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index c70ce9bebcc..71d8e06de76 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -4,6 +4,36 @@ # Object must have name and path db fields and respond to parent and parent_changed? methods. module Routable extend ActiveSupport::Concern + include CaseSensitivity + + # Finds a Routable object by its full path, without knowing the class. + # + # Usage: + # + # Routable.find_by_full_path('groupname') # -> Group + # Routable.find_by_full_path('groupname/projectname') # -> Project + # + # Returns a single object, or nil. + def self.find_by_full_path(path, follow_redirects: false, route_scope: Route, redirect_route_scope: RedirectRoute) + return unless path.present? + + # Case sensitive match first (it's cheaper and the usual case) + # If we didn't have an exact match, we perform a case insensitive search + # + # We need to qualify the columns with the table name, to support both direct lookups on + # Route/RedirectRoute, and scoped lookups through the Routable classes. + route = + route_scope.find_by(routes: { path: path }) || + route_scope.iwhere(Route.arel_table[:path] => path).take + + if follow_redirects + route ||= redirect_route_scope.iwhere(RedirectRoute.arel_table[:path] => path).take + end + + return unless route + + route.is_a?(Routable) ? route : route.source + end included do # Remove `inverse_of: source` when upgraded to rails 5.2 @@ -30,15 +60,14 @@ module Routable # # Returns a single object, or nil. def find_by_full_path(path, follow_redirects: false) - # Case sensitive match first (it's cheaper and the usual case) - # If we didn't have an exact match, we perform a case insensitive search - found = includes(:route).find_by(routes: { path: path }) || where_full_path_in([path]).take - - return found if found - - if follow_redirects - joins(:redirect_routes).find_by("LOWER(redirect_routes.path) = LOWER(?)", path) - end + # TODO: Optimize these queries by avoiding joins + # https://gitlab.com/gitlab-org/gitlab/-/issues/292252 + Routable.find_by_full_path( + path, + follow_redirects: follow_redirects, + route_scope: includes(:route).references(:routes), + redirect_route_scope: joins(:redirect_routes) + ) end # Builds a relation to find multiple objects by their full paths. diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index a1f83884f02..535cf25eb9d 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -57,6 +57,13 @@ module TokenAuthenticatable token = read_attribute(token_field) token.present? && ActiveSupport::SecurityUtils.secure_compare(other_token, token) end + + # Base strategy delegates to this method for formatting a token before + # calling set_token. Can be overridden in models to e.g. add a prefix + # to the tokens + mod.define_method("format_#{token_field}") do |token| + token + end end def token_authenticatable_module diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb index aafd0b538a3..f72a41f06b1 100644 --- a/app/models/concerns/token_authenticatable_strategies/base.rb +++ b/app/models/concerns/token_authenticatable_strategies/base.rb @@ -18,10 +18,15 @@ module TokenAuthenticatableStrategies raise NotImplementedError end - def set_token(instance) + def set_token(instance, token) raise NotImplementedError end + # Default implementation returns the token as-is + def format_token(instance, token) + instance.send("format_#{@token_field}", token) # rubocop:disable GitlabSecurity/PublicSend + end + def ensure_token(instance) write_new_token(instance) unless token_set?(instance) get_token(instance) @@ -57,7 +62,8 @@ module TokenAuthenticatableStrategies def write_new_token(instance) new_token = generate_available_token - set_token(instance, new_token) + formatted_token = format_token(instance, new_token) + set_token(instance, formatted_token) end def unique diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 5aa5f2c842b..3b07551fe05 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -9,7 +9,9 @@ class PersonalAccessToken < ApplicationRecord add_authentication_token_field :token, digest: true REDIS_EXPIRY_TIME = 3.minutes - TOKEN_LENGTH = 20 + + # PATs are 20 characters + optional configurable settings prefix (0..20) + TOKEN_LENGTH_RANGE = (20..40).freeze serialize :scopes, Array # rubocop:disable Cop/ActiveRecordSerialize @@ -77,6 +79,15 @@ class PersonalAccessToken < ApplicationRecord ) end + def self.token_prefix + Gitlab::CurrentSettings.current_application_settings.personal_access_token_prefix + end + + override :format_token + def format_token(token) + "#{self.class.token_prefix}#{token}" + end + protected def validate_scopes diff --git a/app/models/redirect_route.rb b/app/models/redirect_route.rb index 22f60802257..749f4a87818 100644 --- a/app/models/redirect_route.rb +++ b/app/models/redirect_route.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class RedirectRoute < ApplicationRecord + include CaseSensitivity + belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations validates :source, presence: true diff --git a/app/models/route.rb b/app/models/route.rb index fe4846b3be5..fcc8459d6e5 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -4,7 +4,7 @@ class Route < ApplicationRecord include CaseSensitivity include Gitlab::SQL::Pattern - belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations + belongs_to :source, polymorphic: true, inverse_of: :route # rubocop:disable Cop/PolymorphicAssociations validates :source, presence: true validates :path, diff --git a/app/models/user.rb b/app/models/user.rb index bccc6aa6ed2..f1c7644901a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1665,7 +1665,7 @@ class User < ApplicationRecord save end - # each existing user needs to have an `feed_token`. + # each existing user needs to have a `feed_token`. # we do this on read since migrating all existing users is not a feasible # solution. def feed_token diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index f46eb84ce8e..46155f3f670 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -52,6 +52,9 @@ = link_to _('More information'), help_page_path('user/permissions', anchor: 'setting-new-users-to-external'), target: '_blank' .form-group + = f.label :personal_access_token_prefix, _('Personal Access Token prefix'), class: 'label-light' + = f.text_field :personal_access_token_prefix, placeholder: _('Max 20 characters'), class: 'form-control' + .form-group = f.label :user_show_add_ssh_key_message, _('Prompt users to upload SSH keys'), class: 'label-bold' .form-check = f.check_box :user_show_add_ssh_key_message, class: 'form-check-input' diff --git a/app/views/import/bulk_imports/status.html.haml b/app/views/import/bulk_imports/status.html.haml index e6511b46bbc..80b96a25ebb 100644 --- a/app/views/import/bulk_imports/status.html.haml +++ b/app/views/import/bulk_imports/status.html.haml @@ -6,3 +6,7 @@ = s_('ImportGroups|Import groups from GitLab') %p.gl-my-0.gl-py-5.gl-border-solid.gl-border-gray-200.gl-border-0.gl-border-b-1 = s_('ImportGroups|Importing groups from %{link}').html_safe % { link: external_link(@source_url, @source_url) } + +#import-groups-mount-element{ data: { status_path: status_import_bulk_imports_path(format: :json), + available_namespaces_path: import_available_namespaces_path(format: :json), + create_bulk_import_path: import_bulk_imports_path(format: :json) } } diff --git a/app/views/shared/wikis/_sidebar.html.haml b/app/views/shared/wikis/_sidebar.html.haml index 7022762840e..a906bf7aa63 100644 --- a/app/views/shared/wikis/_sidebar.html.haml +++ b/app/views/shared/wikis/_sidebar.html.haml @@ -4,11 +4,10 @@ %a.gutter-toggle.float-right.d-block.d-md-none.js-sidebar-wiki-toggle{ href: "#" } = sprite_icon('chevron-double-lg-right', css_class: 'gl-icon') - - if @wiki.container.is_a?(Project) - - git_access_url = wiki_path(@wiki, action: :git_access) - = link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '', data: { qa_selector: 'clone_repository_link' } do - = sprite_icon('download', css_class: 'gl-mr-2') - %span= _("Clone repository") + - git_access_url = wiki_path(@wiki, action: :git_access) + = link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '', data: { qa_selector: 'clone_repository_link' } do + = sprite_icon('download', css_class: 'gl-mr-2') + %span= _("Clone repository") - if @sidebar_error.present? = render 'shared/alert_info', body: s_('Wiki|The sidebar failed to load. You can reload the page to try again.') diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/shared/wikis/git_access.html.haml index 2542860c742..2542860c742 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/shared/wikis/git_access.html.haml diff --git a/changelogs/unreleased/207869-group-wikis-git-support.yml b/changelogs/unreleased/207869-group-wikis-git-support.yml new file mode 100644 index 00000000000..9eb8f2f2434 --- /dev/null +++ b/changelogs/unreleased/207869-group-wikis-git-support.yml @@ -0,0 +1,5 @@ +--- +title: Support Git access for group wikis +merge_request: 45892 +author: +type: added diff --git a/changelogs/unreleased/change_unique_index_on_security_findings.yml b/changelogs/unreleased/change_unique_index_on_security_findings.yml new file mode 100644 index 00000000000..47360fd6394 --- /dev/null +++ b/changelogs/unreleased/change_unique_index_on_security_findings.yml @@ -0,0 +1,5 @@ +--- +title: Change the unique index on `security_findings` table +merge_request: 50046 +author: +type: changed diff --git a/changelogs/unreleased/feat-token-prefix.yml b/changelogs/unreleased/feat-token-prefix.yml new file mode 100644 index 00000000000..228880cf9e4 --- /dev/null +++ b/changelogs/unreleased/feat-token-prefix.yml @@ -0,0 +1,5 @@ +--- +title: Configurable personal access token prefix +merge_request: 20968 +author: 'Max Wittig & Diego Louzán' +type: added diff --git a/changelogs/unreleased/georgekoltsov-update-project-imported-usage-ping.yml b/changelogs/unreleased/georgekoltsov-update-project-imported-usage-ping.yml new file mode 100644 index 00000000000..1751500b2d3 --- /dev/null +++ b/changelogs/unreleased/georgekoltsov-update-project-imported-usage-ping.yml @@ -0,0 +1,5 @@ +--- +title: Update projects_imported.total usage metric +merge_request: 49568 +author: +type: fixed diff --git a/config/feature_flags/development/ci_allow_failure_with_exit_codes.yml b/config/feature_flags/development/ci_allow_failure_with_exit_codes.yml new file mode 100644 index 00000000000..c2701705616 --- /dev/null +++ b/config/feature_flags/development/ci_allow_failure_with_exit_codes.yml @@ -0,0 +1,8 @@ +--- +name: ci_allow_failure_with_exit_codes +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49145 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/292024 +milestone: '13.7' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/config/routes/git_http.rb b/config/routes/git_http.rb index f27fc2b79b3..715d4b5cc59 100644 --- a/config/routes/git_http.rb +++ b/config/routes/git_http.rb @@ -9,7 +9,7 @@ scope(path: '*repository_path', format: false) do end # NOTE: LFS routes are exposed on all repository types, but we still check for - # LFS availability on the repository container in LfsRequest#require_lfs_enabled! + # LFS availability on the repository container in LfsRequest#lfs_check_access! # Git LFS API (metadata) scope(path: 'info/lfs/objects', controller: :lfs_api) do diff --git a/db/migrate/20201119133534_add_personal_access_token_prefix_to_application_setting.rb b/db/migrate/20201119133534_add_personal_access_token_prefix_to_application_setting.rb new file mode 100644 index 00000000000..c6bb6b7d514 --- /dev/null +++ b/db/migrate/20201119133534_add_personal_access_token_prefix_to_application_setting.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class AddPersonalAccessTokenPrefixToApplicationSetting < ActiveRecord::Migration[6.0] + DOWNTIME = false + + # rubocop:disable Migration/AddLimitToTextColumns + # limit is added in 20201119133604_add_text_limit_to_application_setting_personal_access_token_prefix + def change + add_column :application_settings, :personal_access_token_prefix, :text + end + # rubocop:enable Migration/AddLimitToTextColumns +end diff --git a/db/migrate/20201119133604_add_text_limit_to_application_setting_personal_access_token_prefix.rb b/db/migrate/20201119133604_add_text_limit_to_application_setting_personal_access_token_prefix.rb new file mode 100644 index 00000000000..a118da9e3e7 --- /dev/null +++ b/db/migrate/20201119133604_add_text_limit_to_application_setting_personal_access_token_prefix.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddTextLimitToApplicationSettingPersonalAccessTokenPrefix < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_text_limit :application_settings, :personal_access_token_prefix, 20 + end + + def down + remove_text_limit :application_settings, :personal_access_token_prefix + end +end diff --git a/db/migrate/20201210101250_add_index_projects_on_import_type_and_creator_id.rb b/db/migrate/20201210101250_add_index_projects_on_import_type_and_creator_id.rb new file mode 100644 index 00000000000..5eb8f1d658e --- /dev/null +++ b/db/migrate/20201210101250_add_index_projects_on_import_type_and_creator_id.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddIndexProjectsOnImportTypeAndCreatorId < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :projects, [:creator_id, :import_type, :created_at], + where: 'import_type IS NOT NULL', + name: 'index_projects_on_creator_id_import_type_and_created_at_partial' + end + + def down + remove_concurrent_index_by_name :projects, 'index_projects_on_creator_id_import_type_and_created_at_partial' + end +end diff --git a/db/migrate/20201215132151_change_unique_index_on_security_findings.rb b/db/migrate/20201215132151_change_unique_index_on_security_findings.rb new file mode 100644 index 00000000000..fe474ef3991 --- /dev/null +++ b/db/migrate/20201215132151_change_unique_index_on_security_findings.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class ChangeUniqueIndexOnSecurityFindings < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + OLD_INDEX_NAME = 'index_security_findings_on_uuid' + NEW_INDEX_NAME = 'index_security_findings_on_uuid_and_scan_id' + + disable_ddl_transaction! + + class SecurityFinding < ActiveRecord::Base + include EachBatch + + self.table_name = 'security_findings' + end + + def up + add_concurrent_index :security_findings, [:uuid, :scan_id], unique: true, name: NEW_INDEX_NAME + + remove_concurrent_index_by_name :security_findings, OLD_INDEX_NAME + end + + def down + # It is very unlikely that we rollback this migration but just in case if we have to, + # we have to clear the table because there can be multiple records with the same UUID + # which would break the creation of unique index on the `uuid` column. + # We choose clearing the table because just removing the duplicated records would + # cause data inconsistencies. + SecurityFinding.each_batch(of: 10000) { |relation| relation.delete_all } + + add_concurrent_index :security_findings, :uuid, unique: true, name: OLD_INDEX_NAME + + remove_concurrent_index_by_name :security_findings, NEW_INDEX_NAME + end +end diff --git a/db/schema_migrations/20201119133534 b/db/schema_migrations/20201119133534 new file mode 100644 index 00000000000..b3999923381 --- /dev/null +++ b/db/schema_migrations/20201119133534 @@ -0,0 +1 @@ +6c8fc7904f50a792e10b5f1b0abe90ba21b1bdfd47430b3caa0df870c0a24079
\ No newline at end of file diff --git a/db/schema_migrations/20201119133604 b/db/schema_migrations/20201119133604 new file mode 100644 index 00000000000..865ce7db9e7 --- /dev/null +++ b/db/schema_migrations/20201119133604 @@ -0,0 +1 @@ +bfb8ac3b697675bd4fca53273c6c6feb2f7a5659cbdaf57b9b4adb3e189b74ad
\ No newline at end of file diff --git a/db/schema_migrations/20201210101250 b/db/schema_migrations/20201210101250 new file mode 100644 index 00000000000..4657c9f264e --- /dev/null +++ b/db/schema_migrations/20201210101250 @@ -0,0 +1 @@ +734ef1c319549df72bbbfe3acf93ca05f7a6c5547a1efdcaba780195181f5f9a
\ No newline at end of file diff --git a/db/schema_migrations/20201215132151 b/db/schema_migrations/20201215132151 new file mode 100644 index 00000000000..e051fb91e12 --- /dev/null +++ b/db/schema_migrations/20201215132151 @@ -0,0 +1 @@ +916f29e6ab89551fd785c3a8584c24b72d9002ada30d159e9ff826cb247199b5
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index df69c8fdfe6..81c21fc0c17 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9372,11 +9372,13 @@ CREATE TABLE application_settings ( secret_detection_revocation_token_types_url text, cloud_license_enabled boolean DEFAULT false NOT NULL, disable_feed_token boolean DEFAULT false NOT NULL, + personal_access_token_prefix text, CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)), CONSTRAINT check_17d9558205 CHECK ((char_length((kroki_url)::text) <= 1024)), CONSTRAINT check_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)), CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)), CONSTRAINT check_57123c9593 CHECK ((char_length(help_page_documentation_base_url) <= 255)), + CONSTRAINT check_718b4458ae CHECK ((char_length(personal_access_token_prefix) <= 20)), CONSTRAINT check_85a39b68ff CHECK ((char_length(encrypted_ci_jwt_signing_key_iv) <= 255)), CONSTRAINT check_9a719834eb CHECK ((char_length(secret_detection_token_revocation_url) <= 255)), CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)), @@ -22275,6 +22277,8 @@ CREATE INDEX index_projects_on_creator_id_and_created_at_and_id ON projects USIN CREATE INDEX index_projects_on_creator_id_and_id ON projects USING btree (creator_id, id); +CREATE INDEX index_projects_on_creator_id_import_type_and_created_at_partial ON projects USING btree (creator_id, import_type, created_at) WHERE (import_type IS NOT NULL); + CREATE INDEX index_projects_on_description_trigram ON projects USING gin (description gin_trgm_ops); CREATE INDEX index_projects_on_id_and_archived_and_pending_delete ON projects USING btree (id) WHERE ((archived = false) AND (pending_delete = false)); @@ -22527,7 +22531,7 @@ CREATE INDEX index_security_findings_on_scanner_id ON security_findings USING bt CREATE INDEX index_security_findings_on_severity ON security_findings USING btree (severity); -CREATE UNIQUE INDEX index_security_findings_on_uuid ON security_findings USING btree (uuid); +CREATE UNIQUE INDEX index_security_findings_on_uuid_and_scan_id ON security_findings USING btree (uuid, scan_id); CREATE INDEX index_self_managed_prometheus_alert_events_on_environment_id ON self_managed_prometheus_alert_events USING btree (environment_id); diff --git a/doc/api/settings.md b/doc/api/settings.md index f9c1a73addf..5680687e87e 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -28,61 +28,62 @@ Example response: ```json { - "default_projects_limit" : 100000, - "signup_enabled" : true, - "id" : 1, - "default_branch_protection" : 2, - "restricted_visibility_levels" : [], - "password_authentication_enabled_for_web" : true, - "after_sign_out_path" : null, - "max_attachment_size" : 10, - "max_import_size": 50, - "user_oauth_applications" : true, - "updated_at" : "2016-01-04T15:44:55.176Z", - "session_expire_delay" : 10080, - "home_page_url" : null, - "default_snippet_visibility" : "private", - "outbound_local_requests_whitelist": [], - "domain_allowlist" : [], - "domain_denylist_enabled" : false, - "domain_denylist" : [], - "created_at" : "2016-01-04T15:44:55.176Z", - "default_ci_config_path" : null, - "default_project_visibility" : "private", - "default_group_visibility" : "private", - "gravatar_enabled" : true, - "sign_in_text" : null, - "container_expiration_policies_enable_historic_entries": true, - "container_registry_token_expire_delay": 5, - "repository_storages_weighted": {"default": 100}, - "plantuml_enabled": false, - "plantuml_url": null, - "kroki_enabled": false, - "kroki_url": null, - "terminal_max_session_time": 0, - "polling_interval_multiplier": 1.0, - "rsa_key_restriction": 0, - "dsa_key_restriction": 0, - "ecdsa_key_restriction": 0, - "ed25519_key_restriction": 0, - "first_day_of_week": 0, - "enforce_terms": true, - "terms": "Hello world!", - "performance_bar_allowed_group_id": 42, - "user_show_add_ssh_key_message": true, - "local_markdown_version": 0, - "allow_local_requests_from_hooks_and_services": true, - "allow_local_requests_from_web_hooks_and_services": true, - "allow_local_requests_from_system_hooks": false, - "asset_proxy_enabled": true, - "asset_proxy_url": "https://assets.example.com", - "asset_proxy_whitelist": ["example.com", "*.example.com", "your-instance.com"], - "npm_package_requests_forwarding": true, - "snippet_size_limit": 52428800, - "issues_create_limit": 300, - "raw_blob_request_limit": 300, - "wiki_page_max_content_bytes": 52428800, - "require_admin_approval_after_user_signup": false + "default_projects_limit" : 100000, + "signup_enabled" : true, + "id" : 1, + "default_branch_protection" : 2, + "restricted_visibility_levels" : [], + "password_authentication_enabled_for_web" : true, + "after_sign_out_path" : null, + "max_attachment_size" : 10, + "max_import_size": 50, + "user_oauth_applications" : true, + "updated_at" : "2016-01-04T15:44:55.176Z", + "session_expire_delay" : 10080, + "home_page_url" : null, + "default_snippet_visibility" : "private", + "outbound_local_requests_whitelist": [], + "domain_allowlist" : [], + "domain_denylist_enabled" : false, + "domain_denylist" : [], + "created_at" : "2016-01-04T15:44:55.176Z", + "default_ci_config_path" : null, + "default_project_visibility" : "private", + "default_group_visibility" : "private", + "gravatar_enabled" : true, + "sign_in_text" : null, + "container_expiration_policies_enable_historic_entries": true, + "container_registry_token_expire_delay": 5, + "repository_storages_weighted": {"default": 100}, + "plantuml_enabled": false, + "plantuml_url": null, + "kroki_enabled": false, + "kroki_url": null, + "terminal_max_session_time": 0, + "polling_interval_multiplier": 1.0, + "rsa_key_restriction": 0, + "dsa_key_restriction": 0, + "ecdsa_key_restriction": 0, + "ed25519_key_restriction": 0, + "first_day_of_week": 0, + "enforce_terms": true, + "terms": "Hello world!", + "performance_bar_allowed_group_id": 42, + "user_show_add_ssh_key_message": true, + "local_markdown_version": 0, + "allow_local_requests_from_hooks_and_services": true, + "allow_local_requests_from_web_hooks_and_services": true, + "allow_local_requests_from_system_hooks": false, + "asset_proxy_enabled": true, + "asset_proxy_url": "https://assets.example.com", + "asset_proxy_whitelist": ["example.com", "*.example.com", "your-instance.com"], + "npm_package_requests_forwarding": true, + "snippet_size_limit": 52428800, + "issues_create_limit": 300, + "raw_blob_request_limit": 300, + "wiki_page_max_content_bytes": 52428800, + "require_admin_approval_after_user_signup": false, + "personal_access_token_prefix": "GL-" } ``` @@ -91,12 +92,12 @@ the `file_template_project_id`, `deletion_adjourned_period`, or the `geo_node_al ```json { - "id" : 1, - "signup_enabled" : true, - "file_template_project_id": 1, - "geo_node_allowed_ips": "0.0.0.0/0, ::/0", - "deletion_adjourned_period": 7, - ... + "id" : 1, + "signup_enabled" : true, + "file_template_project_id": 1, + "geo_node_allowed_ips": "0.0.0.0/0, ::/0", + "deletion_adjourned_period": 7, + ... } ``` @@ -174,7 +175,8 @@ Example response: "issues_create_limit": 300, "raw_blob_request_limit": 300, "wiki_page_max_content_bytes": 52428800, - "require_admin_approval_after_user_signup": false + "require_admin_approval_after_user_signup": false, + "personal_access_token_prefix": "GL-" } ``` @@ -318,6 +320,7 @@ listed in the descriptions of the relevant settings. | `performance_bar_allowed_group_id` | string | no | (Deprecated: Use `performance_bar_allowed_group_path` instead) Path of the group that is allowed to toggle the performance bar. | | `performance_bar_allowed_group_path` | string | no | Path of the group that is allowed to toggle the performance bar. | | `performance_bar_enabled` | boolean | no | (Deprecated: Pass `performance_bar_allowed_group_path: nil` instead) Allow enabling the performance bar. | +| `personal_access_token_prefix` | string | no | Prefix for all generated personal access tokens. | | `plantuml_enabled` | boolean | no | (**If enabled, requires:** `plantuml_url`) Enable PlantUML integration. Default is `false`. | | `plantuml_url` | string | required by: `plantuml_enabled` | The PlantUML instance URL for integration. | | `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to `0` to disable polling. | diff --git a/doc/architecture/blueprints/feature_flags_development/index.md b/doc/architecture/blueprints/feature_flags_development/index.md index 6be582bb8af..c92b4113465 100644 --- a/doc/architecture/blueprints/feature_flags_development/index.md +++ b/doc/architecture/blueprints/feature_flags_development/index.md @@ -119,7 +119,9 @@ This work is being done as part of dedicated epic: [Improve internal usage of Feature Flags](https://gitlab.com/groups/gitlab-org/-/epics/3551). This epic describes a meta reasons for making these changes. -## Who +## [Who](#who) + +### Blueprint Proposal: @@ -140,4 +142,24 @@ DRIs: | Leadership | Craig Gomes | | Engineering | Kamil Trzciński | +### [Stakeholders](#stakeholders) + +| Role | Person | Title +|--------------------|-----------------------|--------------------------------------------------------------------| +| Executive Sponsor | Christopher Lefelhocz | Senior Director of Development | +| Facilitator | Darby Frey | Senior Engineering Manager, Verify | +| DRI / Leadership | Craig Gomes | Backend Engineering Manager, Memory and Database | +| DRI / Engineering | Kamil Trzciński | Distinguished Engineer, Ops and Enablement | +| DRI / Product | Kenny Johnston | Senior Director of Product Management, Ops | +| Functional Lead | Ricky Wiens | Backend Engineering Manager, Verify:Testing | +| Functional Lead | Anthony Sandoval | Engineering Manager, Reliability | +| Functional Lead | James Heimbuck | Senior Product Manager, Verify:Testing | +| Member | Grzegorz Bizon | Staff Backend Engineer, Verify | +| Member | Michelle Gill | Engineering Manager, Create:Source Code | +| Member | Wayne Haber | Director of Engineering, Threat Management | +| Member | Doug Stull | Senior Fullstack Engineer, Growth:Expansion | +| Member | Andrew Fontaine | Senior Frontend Engineer, Release | +| Member | Rémy Coutable | Staff Backend Engineer, Engineering Productivity | +| Member | Marin Jankovski | Senior Engineering Manager, Infrastructure, Delivery & Scalability | + <!-- vale gitlab.Spelling = YES --> diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md index b297a19ca30..9eb15c53b66 100644 --- a/doc/user/admin_area/settings/account_and_limit_settings.md +++ b/doc/user/admin_area/settings/account_and_limit_settings.md @@ -35,6 +35,25 @@ If you choose a size larger than what is currently configured for the web server you will likely get errors. See the [troubleshooting section](#troubleshooting) for more details. +## Personal Access Token prefix + +You can set a global prefix for all generated Personal Access Tokens. + +A prefix can help you identify PATs visually, as well as with automation tools. + +### Setting a prefix + +Only a GitLab administrator can set the prefix, which is a global setting applied +to any PAT generated in the system by any user: + +1. Navigate to **Admin Area > Settings > General**. +1. Expand the **Account and limit** section. +1. Fill in the **Personal Access Token prefix** field. +1. Click **Save changes**. + +It is also possible to configure the prefix via the [settings API](../../../api/settings.md) +using the `personal_access_token_prefix` field. + ## Repository size limit **(STARTER ONLY)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/740) in [GitLab Enterprise Edition 8.12](https://about.gitlab.com/releases/2016/09/22/gitlab-8-12-released/#limit-project-size-ee). diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 7940276cd82..0eac10663ec 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -404,7 +404,7 @@ and above. There are a few limitations compared to project wikis: -- Local Git access is not supported yet. +- Git LFS is not supported. - Group wikis are not included in global search, group exports, backups, and Geo replication. - Changes to group wikis don't show up in the group's activity feed. - Group wikis [can't be moved](../../api/project_repository_storage_moves.md#limitations) using the project diff --git a/lib/api/settings.rb b/lib/api/settings.rb index e68c282030b..b3f09b431b0 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -103,6 +103,7 @@ module API optional :performance_bar_allowed_group_id, type: String, desc: 'Deprecated: Use :performance_bar_allowed_group_path instead. Path of the group that is allowed to toggle the performance bar.' # support legacy names, can be removed in v6 optional :performance_bar_allowed_group_path, type: String, desc: 'Path of the group that is allowed to toggle the performance bar.' optional :performance_bar_enabled, type: String, desc: 'Deprecated: Pass `performance_bar_allowed_group_path: nil` instead. Allow enabling the performance.' # support legacy names, can be removed in v6 + optional :personal_access_token_prefix, type: String, desc: 'Prefix to prepend to all personal access tokens' optional :kroki_enabled, type: Boolean, desc: 'Enable Kroki' given kroki_enabled: ->(val) { val } do requires :kroki_url, type: String, desc: 'The Kroki server URL' diff --git a/lib/gitlab/auth/auth_finders.rb b/lib/gitlab/auth/auth_finders.rb index 223cb227c53..caa881eeeab 100644 --- a/lib/gitlab/auth/auth_finders.rb +++ b/lib/gitlab/auth/auth_finders.rb @@ -194,6 +194,10 @@ module Gitlab def access_token strong_memoize(:access_token) do + # The token can be a PAT or an OAuth (doorkeeper) token + # It is also possible that a PAT is encapsulated in a `Bearer` OAuth token + # (e.g. NPM client registry auth), this case will be properly handled + # by find_personal_access_token find_oauth_access_token || find_personal_access_token end end @@ -237,7 +241,7 @@ module Gitlab end def matches_personal_access_token_length?(token) - token.length == PersonalAccessToken::TOKEN_LENGTH + PersonalAccessToken::TOKEN_LENGTH_RANGE.include?(token.length) end # Check if the request is GET/HEAD, or if CSRF token is valid. diff --git a/lib/gitlab/ci/config/entry/allow_failure.rb b/lib/gitlab/ci/config/entry/allow_failure.rb new file mode 100644 index 00000000000..de768c3a03b --- /dev/null +++ b/lib/gitlab/ci/config/entry/allow_failure.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents allow_failure settings. + # + class AllowFailure < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Attributable + include ::Gitlab::Config::Entry::Validatable + + ALLOWED_KEYS = %i[exit_codes].freeze + attributes ALLOWED_KEYS + + validations do + validates :config, hash_or_boolean: true + validates :config, allowed_keys: ALLOWED_KEYS + validates :exit_codes, array_of_integers_or_integer: true, allow_nil: true + end + + def value + @config[:exit_codes] = Array.wrap(exit_codes) if exit_codes.present? + @config + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/bridge.rb b/lib/gitlab/ci/config/entry/bridge.rb index 70fcc1d586a..e8e2eef281e 100644 --- a/lib/gitlab/ci/config/entry/bridge.rb +++ b/lib/gitlab/ci/config/entry/bridge.rb @@ -22,6 +22,7 @@ module Gitlab in: ALLOWED_WHEN, message: "should be one of: #{ALLOWED_WHEN.join(', ')}" } + validates :allow_failure, boolean: true end validate on: :composed do @@ -47,7 +48,7 @@ module Gitlab inherit: false, metadata: { allowed_needs: %i[job bridge] } - attributes :when + attributes :when, :allow_failure def self.matching?(name, config) !name.to_s.start_with?('.') && @@ -72,6 +73,10 @@ module Gitlab def bridge_needs needs_value[:bridge] if needs_value end + + def ignored? + allow_failure.nil? ? manual_action? : allow_failure + end end end end diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 1ce7060df22..85e3514499c 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -31,6 +31,7 @@ module Gitlab validates :dependencies, array_of_strings: true validates :resource_group, type: String + validates :allow_failure, hash_or_boolean: true end validates :start_in, duration: { limit: '1 week' }, if: :delayed? @@ -117,9 +118,14 @@ module Gitlab description: 'Parallel configuration for this job.', inherit: false + entry :allow_failure, ::Gitlab::Ci::Config::Entry::AllowFailure, + description: 'Indicates whether this job is allowed to fail or not.', + inherit: false + attributes :script, :tags, :when, :dependencies, :needs, :retry, :parallel, :start_in, - :interruptible, :timeout, :resource_group, :release + :interruptible, :timeout, :resource_group, + :release, :allow_failure def self.matching?(name, config) !name.to_s.start_with?('.') && @@ -166,11 +172,32 @@ module Gitlab release: release_value, after_script: after_script_value, ignore: ignored?, + allow_failure_criteria: allow_failure_criteria, needs: needs_defined? ? needs_value : nil, resource_group: resource_group, scheduling_type: needs_defined? ? :dag : :stage ).compact end + + def ignored? + allow_failure_defined? ? static_allow_failure : manual_action? + end + + private + + def allow_failure_criteria + return unless ::Gitlab::Ci::Features.allow_failure_with_exit_codes_enabled? + + if allow_failure_defined? && allow_failure_value.is_a?(Hash) + allow_failure_value + end + end + + def static_allow_failure + return false if allow_failure_value.is_a?(Hash) + + allow_failure_value + end end end end diff --git a/lib/gitlab/ci/config/entry/processable.rb b/lib/gitlab/ci/config/entry/processable.rb index c0315e5f901..5ef8cfbddb7 100644 --- a/lib/gitlab/ci/config/entry/processable.rb +++ b/lib/gitlab/ci/config/entry/processable.rb @@ -32,7 +32,6 @@ module Gitlab with_options allow_nil: true do validates :extends, array_of_strings_or_string: true validates :rules, array_of_hashes: true - validates :allow_failure, boolean: true end end @@ -65,7 +64,7 @@ module Gitlab inherit: false, default: {} - attributes :extends, :rules, :allow_failure + attributes :extends, :rules end def compose!(deps = nil) @@ -141,10 +140,6 @@ module Gitlab def manual_action? self.when == 'manual' end - - def ignored? - allow_failure.nil? ? manual_action? : allow_failure - end end end end diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb index 3c679dce9aa..c4dca298839 100644 --- a/lib/gitlab/ci/features.rb +++ b/lib/gitlab/ci/features.rb @@ -62,6 +62,10 @@ module Gitlab def self.ci_pipeline_editor_page_enabled?(project) ::Feature.enabled?(:ci_pipeline_editor_page, project, default_enabled: false) end + + def self.allow_failure_with_exit_codes_enabled? + ::Feature.enabled?(:ci_allow_failure_with_exit_codes) + end end end end diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb index 91dbcc616ea..105221dd6af 100644 --- a/lib/gitlab/ci/pipeline/seed/build.rb +++ b/lib/gitlab/ci/pipeline/seed/build.rb @@ -60,6 +60,7 @@ module Gitlab @seed_attributes .deep_merge(pipeline_attributes) .deep_merge(rules_attributes) + .deep_merge(allow_failure_criteria_attributes) .deep_merge(cache_attributes) end @@ -154,9 +155,13 @@ module Gitlab end def rules_attributes - return {} unless @using_rules - - rules_result.build_attributes + strong_memoize(:rules_attributes) do + if @using_rules + rules_result.build_attributes + else + {} + end + end end def rules_result @@ -176,6 +181,17 @@ module Gitlab @cache.build_attributes end end + + # If a job uses `allow_failure:exit_codes` and `rules:allow_failure` + # we need to prevent the exit codes from being persisted because they + # would break the behavior defined by `rules:allow_failure`. + def allow_failure_criteria_attributes + return {} unless ::Gitlab::Ci::Features.allow_failure_with_exit_codes_enabled? + return {} if rules_attributes[:allow_failure].nil? + return {} unless @seed_attributes.dig(:options, :allow_failure_criteria) + + { options: { allow_failure_criteria: nil } } + end end end end diff --git a/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml index 401a710182d..135f0df99fe 100644 --- a/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/API-Fuzzing.gitlab-ci.yml @@ -15,7 +15,8 @@ variables: FUZZAPI_VERSION: latest FUZZAPI_CONFIG: .gitlab-api-fuzzing.yml FUZZAPI_TIMEOUT: 30 - FUZZAPI_REPORT: gl-api-fuzzing-report.xml + FUZZAPI_REPORT: gl-api-fuzzing-report.json + FUZZAPI_REPORT_ASSET_PATH: assets # FUZZAPI_D_NETWORK: testing-net # @@ -45,6 +46,7 @@ apifuzzer_fuzz: variables: FUZZAPI_PROJECT: $CI_PROJECT_PATH FUZZAPI_API: http://apifuzzer:80 + FUZZAPI_NEW_REPORT: 1 TZ: America/Los_Angeles services: - name: $FUZZAPI_IMAGE @@ -75,6 +77,9 @@ apifuzzer_fuzz: # Run user provided pre-script - sh -c "$FUZZAPI_PRE_SCRIPT" # + # Make sure asset path exists + - mkdir -p $FUZZAPI_REPORT_ASSET_PATH + # # Start scanning - worker-entry # @@ -82,8 +87,12 @@ apifuzzer_fuzz: - sh -c "$FUZZAPI_POST_SCRIPT" # artifacts: + when: always + paths: + - $FUZZAPI_REPORT_ASSET_PATH + - $FUZZAPI_REPORT reports: - junit: $FUZZAPI_REPORT + api_fuzzing: $FUZZAPI_REPORT apifuzzer_fuzz_dnd: stage: fuzz @@ -115,6 +124,9 @@ apifuzzer_fuzz_dnd: # Run user provided pre-script - sh -c "$FUZZAPI_PRE_SCRIPT" # + # Make sure asset path exists + - mkdir -p $FUZZAPI_REPORT_ASSET_PATH + # # Start peach testing engine container - | docker run -d \ @@ -155,6 +167,8 @@ apifuzzer_fuzz_dnd: -e FUZZAPI_PROFILE \ -e FUZZAPI_CONFIG \ -e FUZZAPI_REPORT \ + -e FUZZAPI_REPORT_ASSET_PATH \ + -e FUZZAPI_NEW_REPORT=1 \ -e FUZZAPI_HAR \ -e FUZZAPI_OPENAPI \ -e FUZZAPI_POSTMAN_COLLECTION \ @@ -168,6 +182,8 @@ apifuzzer_fuzz_dnd: -e FUZZAPI_SERVICE_START_TIMEOUT \ -e FUZZAPI_HTTP_USERNAME \ -e FUZZAPI_HTTP_PASSWORD \ + -e CI_PROJECT_URL \ + -e CI_JOB_ID \ -e CI_COMMIT_BRANCH=${CI_COMMIT_BRANCH} \ $FUZZAPI_D_WORKER_ENV \ $FUZZAPI_D_WORKER_PORTS \ @@ -193,6 +209,8 @@ apifuzzer_fuzz_dnd: -e FUZZAPI_PROFILE \ -e FUZZAPI_CONFIG \ -e FUZZAPI_REPORT \ + -e FUZZAPI_REPORT_ASSET_PATH \ + -e FUZZAPI_NEW_REPORT=1 \ -e FUZZAPI_HAR \ -e FUZZAPI_OPENAPI \ -e FUZZAPI_POSTMAN_COLLECTION \ @@ -206,7 +224,10 @@ apifuzzer_fuzz_dnd: -e FUZZAPI_SERVICE_START_TIMEOUT \ -e FUZZAPI_HTTP_USERNAME \ -e FUZZAPI_HTTP_PASSWORD \ + -e CI_PROJECT_URL \ + -e CI_JOB_ID \ -v $CI_PROJECT_DIR:/app \ + -v `pwd`/$FUZZAPI_REPORT_ASSET_PATH:/app/$FUZZAPI_REPORT_ASSET_PATH:rw \ -p 81:80 \ -p 8001:8000 \ -p 515:514 \ @@ -239,7 +260,9 @@ apifuzzer_fuzz_dnd: paths: - ./gl-api_fuzzing*.log - ./gl-api_fuzzing*.zip + - $FUZZAPI_REPORT_ASSET_PATH + - $FUZZAPI_REPORT reports: - junit: $FUZZAPI_REPORT + api_fuzzing: $FUZZAPI_REPORT # end diff --git a/lib/gitlab/ci/yaml_processor/result.rb b/lib/gitlab/ci/yaml_processor/result.rb index 52a00e41214..cd7d781a574 100644 --- a/lib/gitlab/ci/yaml_processor/result.rb +++ b/lib/gitlab/ci/yaml_processor/result.rb @@ -77,6 +77,7 @@ module Gitlab options: { image: job[:image], services: job[:services], + allow_failure_criteria: job[:allow_failure_criteria], artifacts: job[:artifacts], dependencies: job[:dependencies], cross_dependencies: job.dig(:needs, :cross_dependency), diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb index 2a386657e0b..88786ed82ff 100644 --- a/lib/gitlab/config/entry/validators.rb +++ b/lib/gitlab/config/entry/validators.rb @@ -134,6 +134,16 @@ module Gitlab end end + class HashOrBooleanValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless value.is_a?(Hash) || validate_boolean(value) + record.errors.add(attribute, 'should be a hash or a boolean value') + end + end + end + class KeyValidator < ActiveModel::EachValidator include LegacyValidationHelpers @@ -158,6 +168,22 @@ module Gitlab end end + class ArrayOfIntegersOrIntegerValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_integer(value) || validate_array_of_integers(value) + record.errors.add(attribute, 'should be an array of integers or an integer') + end + end + + private + + def validate_array_of_integers(values) + values.is_a?(Array) && values.all? { |value| validate_integer(value) } + end + end + class RegexpValidator < ActiveModel::EachValidator include LegacyValidationHelpers diff --git a/lib/gitlab/gl_repository.rb b/lib/gitlab/gl_repository.rb index 352a93817be..d123989ef8e 100644 --- a/lib/gitlab/gl_repository.rb +++ b/lib/gitlab/gl_repository.rb @@ -20,6 +20,7 @@ module Gitlab end, container_class: ProjectWiki, project_resolver: -> (wiki) { wiki.try(:project) }, + guest_read_ability: :download_wiki_code, suffix: :wiki ).freeze SNIPPET = RepoType.new( diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb index 79cf081b9dc..46c84107e0f 100644 --- a/lib/gitlab/repo_path.rb +++ b/lib/gitlab/repo_path.rb @@ -4,6 +4,13 @@ module Gitlab module RepoPath NotFoundError = Class.new(StandardError) + # Returns an array containing: + # - The repository container + # - The related project (if available) + # - The repository type + # - The original container path (if redirected) + # + # @returns [HasRepository, Project, String, String] def self.parse(path) repo_path = path.delete_prefix('/').delete_suffix('.git') redirected_path = nil @@ -30,7 +37,15 @@ module Gitlab [nil, nil, Gitlab::GlRepository.default_type, nil] end + # Returns an array containing: + # - The repository container + # - The related project (if available) + # - The original container path (if redirected) + # + # @returns [HasRepository, Project, String] def self.find_container(type, full_path) + return [nil, nil, nil] if full_path.blank? + if type.snippet? snippet, redirected_path = find_snippet(full_path) @@ -47,26 +62,24 @@ module Gitlab end def self.find_project(project_path) - return [nil, nil] if project_path.blank? - project = Project.find_by_full_path(project_path, follow_redirects: true) - redirected_path = redirected?(project, project_path) ? project_path : nil + redirected_path = project_path if redirected?(project, project_path) [project, redirected_path] end - def self.redirected?(project, project_path) - project && project.full_path.casecmp(project_path) != 0 + def self.redirected?(container, container_path) + container && container.full_path.casecmp(container_path) != 0 end # Snippet_path can be either: # - snippets/1 # - h5bp/html5-boilerplate/snippets/53 def self.find_snippet(snippet_path) - return [nil, nil] if snippet_path.blank? - snippet_id, project_path = extract_snippet_info(snippet_path) - project, redirected_path = find_project(project_path) + return [nil, nil] unless snippet_id + + project, redirected_path = find_project(project_path) if project_path [Snippet.find_by_id_and_project(id: snippet_id, project: project), redirected_path] end @@ -74,19 +87,23 @@ module Gitlab # Wiki path can be either: # - namespace/project # - group/subgroup/project - def self.find_wiki(wiki_path) - return [nil, nil] if wiki_path.blank? - - project, redirected_path = find_project(wiki_path) - - [project&.wiki, redirected_path] + # + # And also in EE: + # - group + # - group/subgroup + def self.find_wiki(container_path) + container = Routable.find_by_full_path(container_path, follow_redirects: true) + redirected_path = container_path if redirected?(container, container_path) + + # In CE, Group#wiki is not available so this will return nil for a group path. + [container&.try(:wiki), redirected_path] end def self.extract_snippet_info(snippet_path) path_segments = snippet_path.split('/') snippet_id = path_segments.pop - path_segments.pop # Remove snippets from path - project_path = File.join(path_segments) + path_segments.pop # Remove 'snippets' from path + project_path = File.join(path_segments).presence [snippet_id, project_path] end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index e7ee6c3d846..f935c677930 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -584,7 +584,7 @@ module Gitlab gitlab: distinct_count(::BulkImport.where(time_period, source_type: :gitlab), :user_id) }, projects_imported: { - total: count(Project.where(time_period).where.not(import_type: nil)), + total: distinct_count(::Project.where(time_period).where.not(import_type: nil), :creator_id), gitlab_project: projects_imported_count('gitlab_project', time_period), gitlab: projects_imported_count('gitlab', time_period), github: projects_imported_count('github', time_period), @@ -894,7 +894,7 @@ module Gitlab end def projects_imported_count(from, time_period) - distinct_count(::Project.imported_from(from).where(time_period), :creator_id) # rubocop: disable CodeReuse/ActiveRecord + distinct_count(::Project.imported_from(from).where(time_period).where.not(import_type: nil), :creator_id) # rubocop: disable CodeReuse/ActiveRecord end # rubocop:disable CodeReuse/ActiveRecord diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1201cafb85c..4401ee1193a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4774,6 +4774,12 @@ msgstr "" msgid "Bulk request concurrency" msgstr "" +msgid "BulkImport|From source group" +msgstr "" + +msgid "BulkImport|To new group" +msgstr "" + msgid "BulkImport|expected an associated Group but has an associated Project" msgstr "" @@ -16876,6 +16882,9 @@ msgstr "" msgid "Max 100,000 events" msgstr "" +msgid "Max 20 characters" +msgstr "" + msgid "Max Group Export Download requests per minute per user" msgstr "" @@ -20173,6 +20182,9 @@ msgstr "" msgid "Personal Access Token" msgstr "" +msgid "Personal Access Token prefix" +msgstr "" + msgid "Personal project creation is not allowed. Please contact your administrator with questions" msgstr "" @@ -32331,6 +32343,9 @@ msgstr "" msgid "by" msgstr "" +msgid "can contain only letters of the Base64 alphabet (RFC4648) with the addition of '@', ':' and '.'" +msgstr "" + msgid "cannot be a date in the past" msgstr "" diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb index f71f859a704..f0b224484c6 100644 --- a/spec/controllers/admin/application_settings_controller_spec.rb +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -157,6 +157,44 @@ RSpec.describe Admin::ApplicationSettingsController do expect(ApplicationSetting.current.default_branch_name).to eq("example_branch_name") end + context "personal access token prefix settings" do + let(:application_settings) { ApplicationSetting.current } + + shared_examples "accepts prefix setting" do |prefix| + it "updates personal_access_token_prefix setting" do + put :update, params: { application_setting: { personal_access_token_prefix: prefix } } + + expect(response).to redirect_to(general_admin_application_settings_path) + expect(application_settings.reload.personal_access_token_prefix).to eq(prefix) + end + end + + shared_examples "rejects prefix setting" do |prefix| + it "does not update personal_access_token_prefix setting" do + put :update, params: { application_setting: { personal_access_token_prefix: prefix } } + + expect(response).not_to redirect_to(general_admin_application_settings_path) + expect(application_settings.reload.personal_access_token_prefix).not_to eq(prefix) + end + end + + context "with valid prefix" do + include_examples("accepts prefix setting", "a_prefix@") + end + + context "with blank prefix" do + include_examples("accepts prefix setting", "") + end + + context "with too long prefix" do + include_examples("rejects prefix setting", "a_prefix@" * 10) + end + + context "with invalid characters prefix" do + include_examples("rejects prefix setting", "a_préfixñ:") + end + end + context 'external policy classification settings' do let(:settings) do { diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb index 4b1ec7850e0..551abf9241d 100644 --- a/spec/controllers/repositories/git_http_controller_spec.rb +++ b/spec/controllers/repositories/git_http_controller_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' RSpec.describe Repositories::GitHttpController do - include GitHttpHelpers - let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:personal_snippet) { create(:personal_snippet, :public, :repository) } let_it_be(:project_snippet) { create(:project_snippet, :public, :repository, project: project) } diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb index d8ff2e08657..9625fdc195d 100644 --- a/spec/factories/personal_access_tokens.rb +++ b/spec/factories/personal_access_tokens.rb @@ -26,5 +26,9 @@ FactoryBot.define do trait :invalid do token_digest { nil } end + + trait :no_prefix do + after(:build) { |personal_access_token| personal_access_token.set_token(Devise.friendly_token) } + end end end diff --git a/spec/features/groups/import_export/connect_instance_spec.rb b/spec/features/groups/import_export/connect_instance_spec.rb index abb4b30fc2d..c0f967fd0b9 100644 --- a/spec/features/groups/import_export/connect_instance_spec.rb +++ b/spec/features/groups/import_export/connect_instance_spec.rb @@ -22,6 +22,19 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do it 'successfully connects to remote instance' do source_url = 'https://gitlab.com' pat = 'demo-pat' + stub_path = 'stub-group' + + stub_request(:get, "%{url}/api/v4/groups?page=1&per_page=30&top_level_only=true" % { url: source_url }).to_return( + body: [{ + id: 2595438, + web_url: 'https://gitlab.com/groups/auto-breakfast', + name: 'Stub', + path: stub_path, + full_name: 'Stub', + full_path: stub_path + }].to_json, + headers: { 'Content-Type' => 'application/json' } + ) expect(page).to have_content 'Import groups from another instance of GitLab' @@ -31,6 +44,7 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do click_on 'Connect instance' expect(page).to have_content 'Importing groups from %{url}' % { url: source_url } + expect(page).to have_content stub_path end end diff --git a/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js new file mode 100644 index 00000000000..d88a31a0e47 --- /dev/null +++ b/spec/frontend/import_entities/import_groups/components/import_table_row_spec.js @@ -0,0 +1,112 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlButton, GlLink, GlFormInput } from '@gitlab/ui'; +import Select2Select from '~/vue_shared/components/select2_select.vue'; +import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue'; +import { STATUSES } from '~/import_entities/constants'; +import { availableNamespacesFixture } from '../graphql/fixtures'; + +const getFakeGroup = status => ({ + web_url: 'https://fake.host/', + full_path: 'fake_group_1', + full_name: 'fake_name_1', + import_target: { + target_namespace: 'root', + new_name: 'group1', + }, + id: 1, + status, +}); + +describe('import table row', () => { + let wrapper; + let group; + + const findByText = (cmp, text) => { + return wrapper.findAll(cmp).wrappers.find(node => node.text().indexOf(text) === 0); + }; + const findImportButton = () => findByText(GlButton, 'Import'); + const findNameInput = () => wrapper.find(GlFormInput); + const findNamespaceDropdown = () => wrapper.find(Select2Select); + + const createComponent = props => { + wrapper = shallowMount(ImportTableRow, { + propsData: { + availableNamespaces: availableNamespacesFixture, + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('events', () => { + beforeEach(() => { + group = getFakeGroup(STATUSES.NONE); + createComponent({ group }); + }); + + it.each` + selector | sourceEvent | payload | event + ${findNamespaceDropdown} | ${'input'} | ${'demo'} | ${'update-target-namespace'} + ${findNameInput} | ${'input'} | ${'demo'} | ${'update-new-name'} + ${findImportButton} | ${'click'} | ${undefined} | ${'import-group'} + `('invokes $event', ({ selector, sourceEvent, payload, event }) => { + selector().vm.$emit(sourceEvent, payload); + expect(wrapper.emitted(event)).toBeDefined(); + expect(wrapper.emitted(event)[0][0]).toBe(payload); + }); + }); + + describe('when entity status is NONE', () => { + beforeEach(() => { + group = getFakeGroup(STATUSES.NONE); + createComponent({ group }); + }); + + it('renders Import button', () => { + expect(findByText(GlButton, 'Import').exists()).toBe(true); + }); + + it('renders namespace dropdown as not disabled', () => { + expect(findNamespaceDropdown().attributes('disabled')).toBe(undefined); + }); + }); + + describe('when entity status is SCHEDULING', () => { + beforeEach(() => { + group = getFakeGroup(STATUSES.SCHEDULING); + createComponent({ group }); + }); + + it('does not render Import button', () => { + expect(findByText(GlButton, 'Import')).toBe(undefined); + }); + + it('renders namespace dropdown as disabled', () => { + expect(findNamespaceDropdown().attributes('disabled')).toBe('true'); + }); + }); + + describe('when entity status is FINISHED', () => { + beforeEach(() => { + group = getFakeGroup(STATUSES.FINISHED); + createComponent({ group }); + }); + + it('does not render Import button', () => { + expect(findByText(GlButton, 'Import')).toBe(undefined); + }); + + it('does not render namespace dropdown', () => { + expect(findNamespaceDropdown().exists()).toBe(false); + }); + + it('renders target as link', () => { + const TARGET_LINK = `${group.import_target.target_namespace}/${group.import_target.new_name}`; + expect(findByText(GlLink, TARGET_LINK).exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js new file mode 100644 index 00000000000..0ca721cd951 --- /dev/null +++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js @@ -0,0 +1,103 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import { GlLoadingIcon } from '@gitlab/ui'; +import waitForPromises from 'helpers/wait_for_promises'; +import createMockApollo from 'jest/helpers/mock_apollo_helper'; +import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue'; +import ImportTable from '~/import_entities/import_groups/components/import_table.vue'; +import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql'; +import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql'; +import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql'; + +import { STATUSES } from '~/import_entities/constants'; + +import { availableNamespacesFixture, generateFakeEntry } from '../graphql/fixtures'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +describe('import table', () => { + let wrapper; + let apolloProvider; + + const createComponent = ({ bulkImportSourceGroups }) => { + apolloProvider = createMockApollo([], { + Query: { + availableNamespaces: () => availableNamespacesFixture, + bulkImportSourceGroups, + }, + Mutation: { + setTargetNamespace: jest.fn(), + setNewName: jest.fn(), + importGroup: jest.fn(), + }, + }); + + wrapper = shallowMount(ImportTable, { + localVue, + apolloProvider, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('renders loading icon while performing request', async () => { + createComponent({ + bulkImportSourceGroups: () => new Promise(() => {}), + }); + await waitForPromises(); + + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + }); + + it('does not renders loading icon when request is completed', async () => { + createComponent({ + bulkImportSourceGroups: () => [], + }); + await waitForPromises(); + + expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + }); + + it('renders import row for each group in response', async () => { + const FAKE_GROUPS = [ + generateFakeEntry({ id: 1, status: STATUSES.NONE }), + generateFakeEntry({ id: 2, status: STATUSES.FINISHED }), + ]; + createComponent({ + bulkImportSourceGroups: () => FAKE_GROUPS, + }); + await waitForPromises(); + + expect(wrapper.findAll(ImportTableRow)).toHaveLength(FAKE_GROUPS.length); + }); + + describe('converts row events to mutation invocations', () => { + const FAKE_GROUP = generateFakeEntry({ id: 1, status: STATUSES.NONE }); + + beforeEach(() => { + createComponent({ + bulkImportSourceGroups: () => [FAKE_GROUP], + }); + return waitForPromises(); + }); + + it.each` + event | payload | mutation | variables + ${'update-target-namespace'} | ${'new-namespace'} | ${setTargetNamespaceMutation} | ${{ sourceGroupId: FAKE_GROUP.id, targetNamespace: 'new-namespace' }} + ${'update-new-name'} | ${'new-name'} | ${setNewNameMutation} | ${{ sourceGroupId: FAKE_GROUP.id, newName: 'new-name' }} + ${'import-group'} | ${undefined} | ${importGroupMutation} | ${{ sourceGroupId: FAKE_GROUP.id }} + `('correctly maps $event to mutation', async ({ event, payload, mutation, variables }) => { + jest.spyOn(apolloProvider.defaultClient, 'mutate'); + wrapper.find(ImportTableRow).vm.$emit(event, payload); + await waitForPromises(); + expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({ + mutation, + variables, + }); + }); + }); +}); diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js new file mode 100644 index 00000000000..e10231dd68b --- /dev/null +++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js @@ -0,0 +1,178 @@ +import MockAdapter from 'axios-mock-adapter'; +import { InMemoryCache } from 'apollo-cache-inmemory'; +import { createMockClient } from 'mock-apollo-client'; +import waitForPromises from 'helpers/wait_for_promises'; +import axios from '~/lib/utils/axios_utils'; +import { + clientTypenames, + createResolvers, +} from '~/import_entities/import_groups/graphql/client_factory'; +import { STATUSES } from '~/import_entities/constants'; + +import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql'; +import availableNamespacesQuery from '~/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql'; +import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql'; +import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql'; +import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql'; +import httpStatus from '~/lib/utils/http_status'; +import { statusEndpointFixture, availableNamespacesFixture } from './fixtures'; + +const FAKE_ENDPOINTS = { + status: '/fake_status_url', + availableNamespaces: '/fake_available_namespaces', + createBulkImport: '/fake_create_bulk_import', +}; + +describe('Bulk import resolvers', () => { + let axiosMockAdapter; + let client; + + beforeEach(() => { + axiosMockAdapter = new MockAdapter(axios); + client = createMockClient({ + cache: new InMemoryCache({ + fragmentMatcher: { match: () => true }, + addTypename: false, + }), + resolvers: createResolvers({ endpoints: FAKE_ENDPOINTS }), + }); + }); + + afterEach(() => { + axiosMockAdapter.restore(); + }); + + describe('queries', () => { + describe('availableNamespaces', () => { + let results; + + beforeEach(async () => { + axiosMockAdapter + .onGet(FAKE_ENDPOINTS.availableNamespaces) + .reply(httpStatus.OK, availableNamespacesFixture); + + const response = await client.query({ query: availableNamespacesQuery }); + results = response.data.availableNamespaces; + }); + + it('mirrors REST endpoint response fields', () => { + const extractRelevantFields = obj => ({ id: obj.id, full_path: obj.full_path }); + + expect(results.map(extractRelevantFields)).toStrictEqual( + availableNamespacesFixture.map(extractRelevantFields), + ); + }); + }); + + describe('bulkImportSourceGroups', () => { + let results; + + beforeEach(async () => { + axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture); + axiosMockAdapter + .onGet(FAKE_ENDPOINTS.availableNamespaces) + .reply(httpStatus.OK, availableNamespacesFixture); + + const response = await client.query({ query: bulkImportSourceGroupsQuery }); + results = response.data.bulkImportSourceGroups; + }); + + it('mirrors REST endpoint response fields', () => { + const MIRRORED_FIELDS = ['id', 'full_name', 'full_path', 'web_url']; + expect( + results.every((r, idx) => + MIRRORED_FIELDS.every( + field => r[field] === statusEndpointFixture.importable_data[idx][field], + ), + ), + ).toBe(true); + }); + + it('populates each result instance with status field default to none', () => { + expect(results.every(r => r.status === STATUSES.NONE)).toBe(true); + }); + + it('populates each result instance with import_target defaulted to first available namespace', () => { + expect( + results.every( + r => r.import_target.target_namespace === availableNamespacesFixture[0].full_path, + ), + ).toBe(true); + }); + }); + }); + + describe('mutations', () => { + let results; + const GROUP_ID = 1; + + beforeEach(() => { + client.writeQuery({ + query: bulkImportSourceGroupsQuery, + data: { + bulkImportSourceGroups: [ + { + __typename: clientTypenames.BulkImportSourceGroup, + id: GROUP_ID, + status: STATUSES.NONE, + web_url: 'https://fake.host/1', + full_path: 'fake_group_1', + full_name: 'fake_name_1', + import_target: { + target_namespace: 'root', + new_name: 'group1', + }, + }, + ], + }, + }); + + client + .watchQuery({ + query: bulkImportSourceGroupsQuery, + fetchPolicy: 'cache-only', + }) + .subscribe(({ data }) => { + results = data.bulkImportSourceGroups; + }); + }); + + it('setTargetNamespaces updates group target namespace', async () => { + const NEW_TARGET_NAMESPACE = 'target'; + await client.mutate({ + mutation: setTargetNamespaceMutation, + variables: { sourceGroupId: GROUP_ID, targetNamespace: NEW_TARGET_NAMESPACE }, + }); + + expect(results[0].import_target.target_namespace).toBe(NEW_TARGET_NAMESPACE); + }); + + it('setNewName updates group target name', async () => { + const NEW_NAME = 'new'; + await client.mutate({ + mutation: setNewNameMutation, + variables: { sourceGroupId: GROUP_ID, newName: NEW_NAME }, + }); + + expect(results[0].import_target.new_name).toBe(NEW_NAME); + }); + + describe('importGroup', () => { + it('sets status to SCHEDULING when request initiates', async () => { + axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(() => new Promise(() => {})); + + client.mutate({ + mutation: importGroupMutation, + variables: { sourceGroupId: GROUP_ID }, + }); + await waitForPromises(); + + const { bulkImportSourceGroups: intermediateResults } = client.readQuery({ + query: bulkImportSourceGroupsQuery, + }); + + expect(intermediateResults[0].status).toBe(STATUSES.SCHEDULING); + }); + }); + }); +}); diff --git a/spec/frontend/import_entities/import_groups/graphql/fixtures.js b/spec/frontend/import_entities/import_groups/graphql/fixtures.js new file mode 100644 index 00000000000..62e9581bd2d --- /dev/null +++ b/spec/frontend/import_entities/import_groups/graphql/fixtures.js @@ -0,0 +1,51 @@ +import { clientTypenames } from '~/import_entities/import_groups/graphql/client_factory'; + +export const generateFakeEntry = ({ id, status, ...rest }) => ({ + __typename: clientTypenames.BulkImportSourceGroup, + web_url: `https://fake.host/${id}`, + full_path: `fake_group_${id}`, + full_name: `fake_name_${id}`, + import_target: { + target_namespace: 'root', + new_name: `group${id}`, + }, + id, + status, + ...rest, +}); + +export const statusEndpointFixture = { + importable_data: [ + { + id: 2595438, + full_name: 'AutoBreakfast', + full_path: 'auto-breakfast', + web_url: 'https://gitlab.com/groups/auto-breakfast', + }, + { + id: 4347861, + full_name: 'GitLab Data', + full_path: 'gitlab-data', + web_url: 'https://gitlab.com/groups/gitlab-data', + }, + { + id: 5723700, + full_name: 'GitLab Services', + full_path: 'gitlab-services', + web_url: 'https://gitlab.com/groups/gitlab-services', + }, + { + id: 349181, + full_name: 'GitLab-examples', + full_path: 'gitlab-examples', + web_url: 'https://gitlab.com/groups/gitlab-examples', + }, + ], +}; + +export const availableNamespacesFixture = [ + { id: 24, full_path: 'Commit451' }, + { id: 22, full_path: 'gitlab-org' }, + { id: 23, full_path: 'gnuwget' }, + { id: 25, full_path: 'jashkenas' }, +]; diff --git a/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js b/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js new file mode 100644 index 00000000000..5940ea544ea --- /dev/null +++ b/spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js @@ -0,0 +1,82 @@ +import { defaultDataIdFromObject } from 'apollo-cache-inmemory'; +import { SourceGroupsManager } from '~/import_entities/import_groups/graphql/services/source_groups_manager'; +import ImportSourceGroupFragment from '~/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql'; +import { clientTypenames } from '~/import_entities/import_groups/graphql/client_factory'; + +describe('SourceGroupsManager', () => { + let manager; + let client; + + const getFakeGroup = () => ({ + __typename: clientTypenames.BulkImportSourceGroup, + id: 5, + }); + + beforeEach(() => { + client = { + readFragment: jest.fn(), + writeFragment: jest.fn(), + }; + + manager = new SourceGroupsManager({ client }); + }); + + it('finds item by group id', () => { + const ID = 5; + + const FAKE_GROUP = getFakeGroup(); + client.readFragment.mockReturnValue(FAKE_GROUP); + const group = manager.findById(ID); + expect(group).toBe(FAKE_GROUP); + expect(client.readFragment).toHaveBeenCalledWith({ + fragment: ImportSourceGroupFragment, + id: defaultDataIdFromObject(getFakeGroup()), + }); + }); + + it('updates group with provided function', () => { + const UPDATED_GROUP = {}; + const fn = jest.fn().mockReturnValue(UPDATED_GROUP); + manager.update(getFakeGroup(), fn); + + expect(client.writeFragment).toHaveBeenCalledWith({ + fragment: ImportSourceGroupFragment, + id: defaultDataIdFromObject(getFakeGroup()), + data: UPDATED_GROUP, + }); + }); + + it('updates group by id with provided function', () => { + const UPDATED_GROUP = {}; + const fn = jest.fn().mockReturnValue(UPDATED_GROUP); + client.readFragment.mockReturnValue(getFakeGroup()); + manager.updateById(getFakeGroup().id, fn); + + expect(client.readFragment).toHaveBeenCalledWith({ + fragment: ImportSourceGroupFragment, + id: defaultDataIdFromObject(getFakeGroup()), + }); + + expect(client.writeFragment).toHaveBeenCalledWith({ + fragment: ImportSourceGroupFragment, + id: defaultDataIdFromObject(getFakeGroup()), + data: UPDATED_GROUP, + }); + }); + + it('sets import status when group is provided', () => { + client.readFragment.mockReturnValue(getFakeGroup()); + + const NEW_STATUS = 'NEW_STATUS'; + manager.setImportStatus(getFakeGroup(), NEW_STATUS); + + expect(client.writeFragment).toHaveBeenCalledWith({ + fragment: ImportSourceGroupFragment, + id: defaultDataIdFromObject(getFakeGroup()), + data: { + ...getFakeGroup(), + status: NEW_STATUS, + }, + }); + }); +}); diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb index 3d048b35015..f927d5912bb 100644 --- a/spec/lib/gitlab/auth/auth_finders_spec.rb +++ b/spec/lib/gitlab/auth/auth_finders_spec.rb @@ -384,6 +384,16 @@ RSpec.describe Gitlab::Auth::AuthFinders do expect { find_personal_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError) end + + context 'when using a non-prefixed access token' do + let(:personal_access_token) { create(:personal_access_token, :no_prefix, user: user) } + + it 'returns user' do + set_header('HTTP_AUTHORIZATION', "Bearer #{personal_access_token.token}") + + expect(find_user_from_access_token).to eq user + end + end end end diff --git a/spec/lib/gitlab/ci/build/rules_spec.rb b/spec/lib/gitlab/ci/build/rules_spec.rb index cbeae33fbcf..ba99650396d 100644 --- a/spec/lib/gitlab/ci/build/rules_spec.rb +++ b/spec/lib/gitlab/ci/build/rules_spec.rb @@ -187,11 +187,17 @@ RSpec.describe Gitlab::Ci::Build::Rules do let(:start_in) { nil } let(:allow_failure) { nil } - subject { Gitlab::Ci::Build::Rules::Result.new(when_value, start_in, allow_failure) } + subject(:result) do + Gitlab::Ci::Build::Rules::Result.new(when_value, start_in, allow_failure) + end describe '#build_attributes' do + subject(:build_attributes) do + result.build_attributes + end + it 'compacts nil values' do - expect(subject.build_attributes).to eq(options: {}, when: 'on_success') + is_expected.to eq(options: {}, when: 'on_success') end end diff --git a/spec/lib/gitlab/ci/config/entry/allow_failure_spec.rb b/spec/lib/gitlab/ci/config/entry/allow_failure_spec.rb new file mode 100644 index 00000000000..7aaad57f5cd --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/allow_failure_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Entry::AllowFailure do + let(:entry) { described_class.new(config.deep_dup) } + let(:expected_config) { config } + + describe 'validations' do + context 'when entry config value is valid' do + shared_examples 'valid entry' do + describe '#value' do + it 'returns key value' do + expect(entry.value).to eq(expected_config) + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'with boolean values' do + it_behaves_like 'valid entry' do + let(:config) { true } + end + + it_behaves_like 'valid entry' do + let(:config) { false } + end + end + + context 'with hash values' do + it_behaves_like 'valid entry' do + let(:config) { { exit_codes: 137 } } + let(:expected_config) { { exit_codes: [137] } } + end + + it_behaves_like 'valid entry' do + let(:config) { { exit_codes: [42, 137] } } + end + end + end + + context 'when entry value is not valid' do + shared_examples 'invalid entry' do + describe '#valid?' do + it { expect(entry).not_to be_valid } + it { expect(entry.errors).to include(error_message) } + end + end + + context 'when it has a wrong type' do + let(:config) { [1] } + let(:error_message) do + 'allow failure config should be a hash or a boolean value' + end + + it_behaves_like 'invalid entry' + end + + context 'with string exit codes' do + let(:config) { { exit_codes: 'string' } } + let(:error_message) do + 'allow failure exit codes should be an array of integers or an integer' + end + + it_behaves_like 'invalid entry' + end + + context 'with array of strings as exit codes' do + let(:config) { { exit_codes: ['string 1', 'string 2'] } } + let(:error_message) do + 'allow failure exit codes should be an array of integers or an integer' + end + + it_behaves_like 'invalid entry' + end + + context 'when it has an extra keys' do + let(:config) { { extra: true } } + let(:error_message) do + 'allow failure config contains unknown keys: extra' + end + + it_behaves_like 'invalid entry' + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb index 8b2e0410474..b3b7901074a 100644 --- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb @@ -227,6 +227,23 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do end end end + + context 'when bridge config contains exit_codes' do + let(:config) do + { script: 'rspec', allow_failure: { exit_codes: [42] } } + end + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns an error message' do + expect(subject.errors) + .to include(/allow failure should be a boolean value/) + end + end + end end describe '#manual_action?' do diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index e0e8bc93770..7834a1a94f2 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -670,6 +670,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do end describe '#ignored?' do + before do + entry.compose! + end + context 'when job is a manual action' do context 'when it is not specified if job is allowed to fail' do let(:config) do @@ -700,6 +704,16 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do expect(entry).not_to be_ignored end end + + context 'when job is dynamically allowed to fail' do + let(:config) do + { script: 'deploy', when: 'manual', allow_failure: { exit_codes: 42 } } + end + + it 'is not an ignored job' do + expect(entry).not_to be_ignored + end + end end context 'when job is not a manual action' do @@ -709,6 +723,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do it 'is not an ignored job' do expect(entry).not_to be_ignored end + + it 'does not return allow_failure' do + expect(entry.value.key?(:allow_failure_criteria)).to be_falsey + end end context 'when job is allowed to fail' do @@ -717,6 +735,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do it 'is an ignored job' do expect(entry).to be_ignored end + + it 'does not return allow_failure_criteria' do + expect(entry.value.key?(:allow_failure_criteria)).to be_falsey + end end context 'when job is not allowed to fail' do @@ -725,6 +747,32 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do it 'is not an ignored job' do expect(entry).not_to be_ignored end + + it 'does not return allow_failure_criteria' do + expect(entry.value.key?(:allow_failure_criteria)).to be_falsey + end + end + + context 'when job is dynamically allowed to fail' do + let(:config) { { script: 'deploy', allow_failure: { exit_codes: 42 } } } + + it 'is not an ignored job' do + expect(entry).not_to be_ignored + end + + it 'returns allow_failure_criteria' do + expect(entry.value[:allow_failure_criteria]).to match(exit_codes: [42]) + end + + context 'with ci_allow_failure_with_exit_codes disabled' do + before do + stub_feature_flags(ci_allow_failure_with_exit_codes: false) + end + + it 'does not return allow_failure_criteria' do + expect(entry.value.key?(:allow_failure_criteria)).to be_falsey + end + end end end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 0b961336f3f..3cbc498c3a4 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -165,6 +165,45 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it { is_expected.to include(options: {}) } end + + context 'with allow_failure' do + let(:options) do + { allow_failure_criteria: { exit_codes: [42] } } + end + + let(:rules) do + [{ if: '$VAR == null', when: 'always' }] + end + + let(:attributes) do + { + name: 'rspec', + ref: 'master', + options: options, + rules: rules + } + end + + context 'when rules does not override allow_failure' do + it { is_expected.to match a_hash_including(options: options) } + end + + context 'when rules set allow_failure to true' do + let(:rules) do + [{ if: '$VAR == null', when: 'always', allow_failure: true }] + end + + it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) } + end + + context 'when rules set allow_failure to false' do + let(:rules) do + [{ if: '$VAR == null', when: 'always', allow_failure: false }] + end + + it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) } + end + end end describe '#bridge?' do diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 411337fc32f..5ad1b3dd241 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -231,6 +231,23 @@ module Gitlab expect(subject[:allow_failure]).to be true end end + + context 'when allow_failure has exit_codes' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + when: 'manual', + allow_failure: { exit_codes: 1 } }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + + it 'saves allow_failure_criteria into options' do + expect(subject[:options]).to match( + a_hash_including(allow_failure_criteria: { exit_codes: [1] })) + end + end end context 'when job is not a manual action' do @@ -254,6 +271,22 @@ module Gitlab expect(subject[:allow_failure]).to be false end end + + context 'when allow_failure is dynamically specified' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + allow_failure: { exit_codes: 1 } }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + + it 'saves allow_failure_criteria into options' do + expect(subject[:options]).to match( + a_hash_including(allow_failure_criteria: { exit_codes: [1] })) + end + end end end @@ -2494,7 +2527,13 @@ module Gitlab context 'returns errors if job allow_failure parameter is not an boolean' do let(:config) { YAML.dump({ rspec: { script: "test", allow_failure: "string" } }) } - it_behaves_like 'returns errors', 'jobs:rspec allow failure should be a boolean value' + it_behaves_like 'returns errors', 'jobs:rspec allow failure should be a hash or a boolean value' + end + + context 'returns errors if job exit_code parameter from allow_failure is not an integer' do + let(:config) { YAML.dump({ rspec: { script: "test", allow_failure: { exit_codes: 'string' } } }) } + + it_behaves_like 'returns errors', 'jobs:rspec:allow_failure exit codes should be an array of integers or an integer' end context 'returns errors if job stage is not a string' do diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index c70ac5f3ca9..c2d96369425 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -224,7 +224,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do gitlab: 2 }, projects_imported: { - total: 20, + total: 2, gitlab_project: 2, gitlab: 2, github: 2, @@ -248,7 +248,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do gitlab: 1 }, projects_imported: { - total: 10, + total: 1, gitlab_project: 1, gitlab: 1, github: 1, diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb index 5fb7cdb4443..7cf7b825d7d 100644 --- a/spec/models/concerns/case_sensitivity_spec.rb +++ b/spec/models/concerns/case_sensitivity_spec.rb @@ -4,16 +4,16 @@ require 'spec_helper' RSpec.describe CaseSensitivity do describe '.iwhere' do - let(:connection) { ActiveRecord::Base.connection } - let(:model) do + let_it_be(:connection) { ActiveRecord::Base.connection } + let_it_be(:model) do Class.new(ActiveRecord::Base) do include CaseSensitivity self.table_name = 'namespaces' end end - let!(:model_1) { model.create!(path: 'mOdEl-1', name: 'mOdEl 1') } - let!(:model_2) { model.create!(path: 'mOdEl-2', name: 'mOdEl 2') } + let_it_be(:model_1) { model.create!(path: 'mOdEl-1', name: 'mOdEl 1') } + let_it_be(:model_2) { model.create!(path: 'mOdEl-2', name: 'mOdEl 2') } it 'finds a single instance by a single attribute regardless of case' do expect(model.iwhere(path: 'MODEL-1')).to contain_exactly(model_1) @@ -28,6 +28,10 @@ RSpec.describe CaseSensitivity do .to contain_exactly(model_1) end + it 'finds instances by custom Arel attributes' do + expect(model.iwhere(model.arel_table[:path] => 'MODEL-1')).to contain_exactly(model_1) + end + it 'builds a query using LOWER' do query = model.iwhere(path: %w(MODEL-1 model-2), name: 'model 1').to_sql expected_query = <<~QRY.strip diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index e4cf68663ef..6ab87053258 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -2,8 +2,69 @@ require 'spec_helper' +RSpec.shared_examples '.find_by_full_path' do + describe '.find_by_full_path', :aggregate_failures do + it 'finds records by their full path' do + expect(described_class.find_by_full_path(record.full_path)).to eq(record) + expect(described_class.find_by_full_path(record.full_path.upcase)).to eq(record) + end + + it 'returns nil for unknown paths' do + expect(described_class.find_by_full_path('unknown')).to be_nil + end + + it 'includes route information when loading a record' do + control_count = ActiveRecord::QueryRecorder.new do + described_class.find_by_full_path(record.full_path) + end.count + + expect do + described_class.find_by_full_path(record.full_path).route + end.not_to exceed_all_query_limit(control_count) + end + + context 'with redirect routes' do + let_it_be(:redirect_route) { create(:redirect_route, source: record) } + + context 'without follow_redirects option' do + it 'does not find records by their redirected path' do + expect(described_class.find_by_full_path(redirect_route.path)).to be_nil + expect(described_class.find_by_full_path(redirect_route.path.upcase)).to be_nil + end + end + + context 'with follow_redirects option set to true' do + it 'finds records by their canonical path' do + expect(described_class.find_by_full_path(record.full_path, follow_redirects: true)).to eq(record) + expect(described_class.find_by_full_path(record.full_path.upcase, follow_redirects: true)).to eq(record) + end + + it 'finds records by their redirected path' do + expect(described_class.find_by_full_path(redirect_route.path, follow_redirects: true)).to eq(record) + expect(described_class.find_by_full_path(redirect_route.path.upcase, follow_redirects: true)).to eq(record) + end + + it 'returns nil for unknown paths' do + expect(described_class.find_by_full_path('unknown', follow_redirects: true)).to be_nil + end + end + end + end +end + +RSpec.describe Routable do + it_behaves_like '.find_by_full_path' do + let_it_be(:record) { create(:group) } + end + + it_behaves_like '.find_by_full_path' do + let_it_be(:record) { create(:project) } + end +end + RSpec.describe Group, 'Routable' do - let!(:group) { create(:group, name: 'foo') } + let_it_be_with_reload(:group) { create(:group, name: 'foo') } + let_it_be(:nested_group) { create(:group, parent: group) } describe 'Validations' do it { is_expected.to validate_presence_of(:route) } @@ -59,61 +120,20 @@ RSpec.describe Group, 'Routable' do end describe '.find_by_full_path' do - let!(:nested_group) { create(:group, parent: group) } - - context 'without any redirect routes' do - it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) } - it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) } - it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) } - it { expect(described_class.find_by_full_path('unknown')).to eq(nil) } - - it 'includes route information when loading a record' do - path = group.to_param - control_count = ActiveRecord::QueryRecorder.new { described_class.find_by_full_path(path) }.count - - expect { described_class.find_by_full_path(path).route }.not_to exceed_all_query_limit(control_count) - end + it_behaves_like '.find_by_full_path' do + let_it_be(:record) { group } end - context 'with redirect routes' do - let!(:group_redirect_route) { group.redirect_routes.create!(path: 'bar') } - let!(:nested_group_redirect_route) { nested_group.redirect_routes.create!(path: nested_group.path.sub('foo', 'bar')) } - - context 'without follow_redirects option' do - context 'with the given path not matching any route' do - it { expect(described_class.find_by_full_path('unknown')).to eq(nil) } - end - - context 'with the given path matching the canonical route' do - it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) } - it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) } - it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) } - end - - context 'with the given path matching a redirect route' do - it { expect(described_class.find_by_full_path(group_redirect_route.path)).to eq(nil) } - it { expect(described_class.find_by_full_path(group_redirect_route.path.upcase)).to eq(nil) } - it { expect(described_class.find_by_full_path(nested_group_redirect_route.path)).to eq(nil) } - end - end - - context 'with follow_redirects option set to true' do - context 'with the given path not matching any route' do - it { expect(described_class.find_by_full_path('unknown', follow_redirects: true)).to eq(nil) } - end + it_behaves_like '.find_by_full_path' do + let_it_be(:record) { nested_group } + end - context 'with the given path matching the canonical route' do - it { expect(described_class.find_by_full_path(group.to_param, follow_redirects: true)).to eq(group) } - it { expect(described_class.find_by_full_path(group.to_param.upcase, follow_redirects: true)).to eq(group) } - it { expect(described_class.find_by_full_path(nested_group.to_param, follow_redirects: true)).to eq(nested_group) } - end + it 'does not find projects with a matching path' do + project = create(:project) + redirect_route = create(:redirect_route, source: project) - context 'with the given path matching a redirect route' do - it { expect(described_class.find_by_full_path(group_redirect_route.path, follow_redirects: true)).to eq(group) } - it { expect(described_class.find_by_full_path(group_redirect_route.path.upcase, follow_redirects: true)).to eq(group) } - it { expect(described_class.find_by_full_path(nested_group_redirect_route.path, follow_redirects: true)).to eq(nested_group) } - end - end + expect(described_class.find_by_full_path(project.full_path)).to be_nil + expect(described_class.find_by_full_path(redirect_route.path, follow_redirects: true)).to be_nil end end @@ -131,8 +151,6 @@ RSpec.describe Group, 'Routable' do end context 'with valid paths' do - let!(:nested_group) { create(:group, parent: group) } - it 'returns the projects matching the paths' do result = described_class.where_full_path_in([group.to_param, nested_group.to_param]) @@ -148,32 +166,36 @@ RSpec.describe Group, 'Routable' do end describe '#full_path' do - let(:group) { create(:group) } - let(:nested_group) { create(:group, parent: group) } - it { expect(group.full_path).to eq(group.path) } it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") } end describe '#full_name' do - let(:group) { create(:group) } - let(:nested_group) { create(:group, parent: group) } - it { expect(group.full_name).to eq(group.name) } it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") } end end RSpec.describe Project, 'Routable' do - describe '#full_path' do - let(:project) { build_stubbed(:project) } + let_it_be(:project) { create(:project) } + it_behaves_like '.find_by_full_path' do + let_it_be(:record) { project } + end + + it 'does not find groups with a matching path' do + group = create(:group) + redirect_route = create(:redirect_route, source: group) + + expect(described_class.find_by_full_path(group.full_path)).to be_nil + expect(described_class.find_by_full_path(redirect_route.path, follow_redirects: true)).to be_nil + end + + describe '#full_path' do it { expect(project.full_path).to eq "#{project.namespace.full_path}/#{project.path}" } end describe '#full_name' do - let(:project) { build_stubbed(:project) } - it { expect(project.full_name).to eq "#{project.namespace.human_name} / #{project.name}" } end end diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index 90e94b5dca9..d8b77e1cd0d 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -105,8 +105,8 @@ RSpec.describe PersonalAccessToken, 'TokenAuthenticatable' do it 'sets new token' do subject - expect(personal_access_token.token).to eq(token_value) - expect(personal_access_token.token_digest).to eq(Gitlab::CryptoHelper.sha256(token_value)) + expect(personal_access_token.token).to eq("#{PersonalAccessToken.token_prefix}#{token_value}") + expect(personal_access_token.token_digest).to eq(Gitlab::CryptoHelper.sha256("#{PersonalAccessToken.token_prefix}#{token_value}")) end end diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index a540c85d4c7..e04f63befd0 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -220,6 +220,8 @@ RSpec.describe API::Internal::Base do end it 'returns a token without expiry when the expires_at parameter is missing' do + token_size = (PersonalAccessToken.token_prefix || '').size + 20 + post api('/internal/personal_access_token'), params: { secret_token: secret_token, @@ -229,12 +231,14 @@ RSpec.describe API::Internal::Base do } expect(json_response['success']).to be_truthy - expect(json_response['token']).to match(/\A\S{20}\z/) + 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 it 'returns a token with expiry when it receives a valid expires_at parameter' do + token_size = (PersonalAccessToken.token_prefix || '').size + 20 + post api('/internal/personal_access_token'), params: { secret_token: secret_token, @@ -245,7 +249,7 @@ RSpec.describe API::Internal::Base do } expect(json_response['success']).to be_truthy - expect(json_response['token']).to match(/\A\S{20}\z/) + 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') end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 03320549e44..8fb0f8fc51a 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -43,6 +43,7 @@ RSpec.describe API::Settings, 'Settings' do expect(json_response['spam_check_endpoint_url']).to be_nil expect(json_response['wiki_page_max_content_bytes']).to be_a(Integer) expect(json_response['require_admin_approval_after_user_signup']).to eq(true) + expect(json_response['personal_access_token_prefix']).to be_nil end end @@ -122,7 +123,8 @@ RSpec.describe API::Settings, 'Settings' do spam_check_endpoint_url: 'https://example.com/spam_check', disabled_oauth_sign_in_sources: 'unknown', import_sources: 'github,bitbucket', - wiki_page_max_content_bytes: 12345 + wiki_page_max_content_bytes: 12345, + personal_access_token_prefix: "GL-" } expect(response).to have_gitlab_http_status(:ok) @@ -166,6 +168,7 @@ RSpec.describe API::Settings, 'Settings' do expect(json_response['disabled_oauth_sign_in_sources']).to eq([]) expect(json_response['import_sources']).to match_array(%w(github bitbucket)) expect(json_response['wiki_page_max_content_bytes']).to eq(12345) + expect(json_response['personal_access_token_prefix']).to eq("GL-") end end @@ -451,5 +454,25 @@ RSpec.describe API::Settings, 'Settings' do expect(json_response['error']).to eq('spam_check_endpoint_url is missing') end end + + context "personal access token prefix settings" do + context "handles validation errors" do + it "fails to update the settings with too long prefix" do + put api("/application/settings", admin), params: { personal_access_token_prefix: "prefix" * 10 } + + expect(response).to have_gitlab_http_status(:bad_request) + message = json_response["message"] + expect(message["personal_access_token_prefix"]).to include(a_string_matching("is too long")) + end + + it "fails to update the settings with invalid characters in the prefix" do + put api("/application/settings", admin), params: { personal_access_token_prefix: "éñ" } + + expect(response).to have_gitlab_http_status(:bad_request) + message = json_response["message"] + expect(message["personal_access_token_prefix"]).to include(a_string_matching("can contain only letters of the Base64 alphabet")) + end + end + end end end diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb index a0ff2fff0ef..a18c8f23ee7 100644 --- a/spec/services/ci/create_pipeline_service/rules_spec.rb +++ b/spec/services/ci/create_pipeline_service/rules_spec.rb @@ -93,6 +93,73 @@ RSpec.describe Ci::CreatePipelineService do end end end + + context 'with allow_failure and exit_codes', :aggregate_failures do + def find_job(name) + pipeline.builds.find_by(name: name) + end + + let(:config) do + <<-EOY + job-1: + script: exit 42 + allow_failure: + exit_codes: 42 + rules: + - if: $CI_COMMIT_REF_NAME == "master" + allow_failure: false + + job-2: + script: exit 42 + allow_failure: + exit_codes: 42 + rules: + - if: $CI_COMMIT_REF_NAME == "master" + allow_failure: true + + job-3: + script: exit 42 + allow_failure: + exit_codes: 42 + rules: + - if: $CI_COMMIT_REF_NAME == "master" + when: manual + EOY + end + + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly( + 'job-1', 'job-2', 'job-3' + ) + end + + it 'assigns job:allow_failure values to the builds' do + expect(find_job('job-1').allow_failure).to eq(false) + expect(find_job('job-2').allow_failure).to eq(true) + expect(find_job('job-3').allow_failure).to eq(false) + end + + it 'removes exit_codes if allow_failure is specified' do + expect(find_job('job-1').options.dig(:allow_failure_criteria)).to be_nil + expect(find_job('job-2').options.dig(:allow_failure_criteria)).to be_nil + expect(find_job('job-3').options.dig(:allow_failure_criteria, :exit_codes)).to eq([42]) + end + + context 'with ci_allow_failure_with_exit_codes disabled' do + before do + stub_feature_flags(ci_allow_failure_with_exit_codes: false) + end + + it 'does not persist allow_failure_criteria' do + expect(pipeline).to be_persisted + + expect(find_job('job-1').options.key?(:allow_failure_criteria)).to be_falsey + expect(find_job('job-2').options.key?(:allow_failure_criteria)).to be_falsey + expect(find_job('job-3').options.key?(:allow_failure_criteria)).to be_falsey + end + end + end end context 'when workflow:rules are used' do diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb index 4c8a45d3b90..dcbf494186a 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -504,6 +504,17 @@ RSpec.shared_examples 'wiki controller actions' do end end + describe '#git_access' do + render_views + + it 'renders the git access page' do + get :git_access, params: routing_params + + expect(response).to render_template('shared/wikis/git_access') + expect(response.body).to include(wiki.http_url_to_repo) + end + end + def redirect_to_wiki(wiki, page) redirect_to(controller.wiki_page_path(wiki, page)) end diff --git a/spec/views/shared/wikis/_sidebar.html.haml_spec.rb b/spec/views/shared/wikis/_sidebar.html.haml_spec.rb index ddf223efd99..3e691862937 100644 --- a/spec/views/shared/wikis/_sidebar.html.haml_spec.rb +++ b/spec/views/shared/wikis/_sidebar.html.haml_spec.rb @@ -16,16 +16,6 @@ RSpec.describe 'shared/wikis/_sidebar.html.haml' do expect(rendered).to have_link('Clone repository') end - context 'the wiki is not a project wiki' do - it 'does not include the clone repository link' do - allow(wiki).to receive(:container).and_return(create(:group)) - - render - - expect(rendered).not_to have_link('Clone repository') - end - end - context 'the sidebar failed to load' do before do assign(:sidebar_error, Object.new) |